/******************************************************************************* * Copyright (c) 2009, 2014 Andrew Gvozdev and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Andrew Gvozdev - initial API and implementation *******************************************************************************/ package org.eclipse.cdt.managedbuilder.language.settings.providers; import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.cdt.core.CCorePlugin; import org.eclipse.cdt.core.EFSExtensionProvider; import org.eclipse.cdt.core.ErrorParserManager; import org.eclipse.cdt.core.cdtvariables.CdtVariableException; import org.eclipse.cdt.core.cdtvariables.ICdtVariableManager; import org.eclipse.cdt.core.language.settings.providers.ICBuildOutputParser; import org.eclipse.cdt.core.language.settings.providers.IWorkingDirectoryTracker; import org.eclipse.cdt.core.language.settings.providers.LanguageSettingsManager; import org.eclipse.cdt.core.language.settings.providers.LanguageSettingsSerializableProvider; import org.eclipse.cdt.core.settings.model.ICConfigurationDescription; import org.eclipse.cdt.core.settings.model.ICLanguageSettingEntry; import org.eclipse.cdt.core.settings.model.ICSettingEntry; import org.eclipse.cdt.core.settings.model.util.CDataUtil; import org.eclipse.cdt.internal.core.XmlUtil; import org.eclipse.cdt.managedbuilder.core.ManagedBuilderCorePlugin; import org.eclipse.cdt.utils.EFSExtensionManager; import org.eclipse.cdt.utils.cdtvariables.CdtVariableResolver; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.content.IContentType; import org.eclipse.core.runtime.content.IContentTypeManager; import org.w3c.dom.Element; /** * Abstract class for language settings providers capable to parse build output. * <p> * <strong>EXPERIMENTAL</strong>. This class interface is not stable yet as * it is not currently (CDT 8.1, Juno) clear how it may need to be used in future. * There is no guarantee that this API will work or that it will remain the same. * Please do not use this API without consulting with the CDT team. * </p> * @noextend This class is not intended to be subclassed by clients. * * @since 8.1 */ public abstract class AbstractLanguageSettingsOutputScanner extends LanguageSettingsSerializableProvider implements ICBuildOutputParser { protected static final String ATTR_KEEP_RELATIVE_PATHS = "keep-relative-paths"; //$NON-NLS-1$ // evaluates to "/${ProjName)/" private static final String PROJ_NAME_PREFIX = '/' + CdtVariableResolver.createVariableReference(CdtVariableResolver.VAR_PROJ_NAME) + '/'; protected ICConfigurationDescription currentCfgDescription = null; protected IWorkingDirectoryTracker cwdTracker = null; protected IProject currentProject = null; protected IResource currentResource = null; protected String currentLanguageId = null; protected String parsedResourceName = null; protected boolean isResolvingPaths = true; /** @since 8.2 */ protected EFSExtensionProvider efsProvider = null; private static final EFSExtensionProvider efsProviderDefault = new EFSExtensionProvider() { final EFSExtensionManager efsManager = EFSExtensionManager.getDefault(); @Override public String getPathFromURI(URI locationURI) { return efsManager.getPathFromURI(locationURI); } @Override public URI getLinkedURI(URI locationURI) { return efsManager.getLinkedURI(locationURI); } @Override public URI createNewURIFromPath(URI locationOnSameFilesystem, String path) { return efsManager.createNewURIFromPath(locationOnSameFilesystem, path); } @Override public String getMappedPath(URI locationURI) { return efsManager.getMappedPath(locationURI); } @Override public boolean isVirtual(URI locationURI) { return efsManager.isVirtual(locationURI); } @Override public URI append(URI baseURI, String extension) { return efsManager.append(baseURI, extension); } }; /** * Abstract class defining common functionality for option parsers. * The purpose of this parser is to parse a portion of string representing * a single option and create a language settings entry out of it. * * See {@link GCCBuildCommandParser} for an example how to define the parsers. */ protected static abstract class AbstractOptionParser { private final int kind; private final String patternStr; private final Pattern pattern; private final String nameExpression; private final String valueExpression; private final int extraFlag; private String parsedName; private String parsedValue; /** * Constructor. * * @param kind - kind of language settings entries being parsed by the parser. * @param pattern - regular expression pattern being parsed by the parser. * @param nameExpression - capturing group expression defining name of an entry. * @param valueExpression - capturing group expression defining value of an entry. * @param extraFlag - extra-flag to add while creating language settings entry. */ public AbstractOptionParser(int kind, String pattern, String nameExpression, String valueExpression, int extraFlag) { this.kind = kind; this.patternStr = pattern; this.nameExpression = nameExpression; this.valueExpression = valueExpression; this.extraFlag = extraFlag; this.pattern = Pattern.compile(pattern); } /** * Create language settings entry of appropriate kind and considering extra-flag passed in constructor. * * @param name - name of language settings entry. * @param value - value of language settings entry. * @param flag - flag to set. Note that the flag will be amended with the extra-flag defined in constructor. * @return new language settings entry. */ public ICLanguageSettingEntry createEntry(String name, String value, int flag) { return (ICLanguageSettingEntry) CDataUtil.createEntry(kind, name, value, null, flag | extraFlag); } /** * Check if the king of option parsed by parser is "file". * * @return {@code true} if the kind is file, {@code false} otherwise. */ public boolean isForFile() { return kind == ICSettingEntry.INCLUDE_FILE || kind == ICSettingEntry.MACRO_FILE; } /** * Check if the king of option parsed by parser is "folder". * * @return {@code true} if the kind is folder, {@code false} otherwise. */ public boolean isForFolder() { return kind == ICSettingEntry.INCLUDE_PATH || kind == ICSettingEntry.LIBRARY_PATH; } /** * Return value represented by the capturing group expression. */ private String parseStr(Matcher matcher, String str) { if (str != null) return matcher.replaceAll(str); return null; } /** * Test for a match and parse a portion of input string representing a single option * to retrieve name and value. * * @param optionString - an option to test and parse, possibly with an argument. * @return {@code true} if the option is a match to parser's regular expression * or {@code false} otherwise. */ public boolean parseOption(String optionString) { // get rid of extra text at the end (for example file name could be confused for an argument) @SuppressWarnings("nls") String option = optionString.replaceFirst("(" + patternStr + ").*", "$1"); Matcher matcher = pattern.matcher(option); boolean isMatch = matcher.matches(); if (isMatch) { parsedName = parseStr(matcher, nameExpression); parsedValue = parseStr(matcher, valueExpression); } return isMatch; } } /** * Implementation of {@link AbstractOptionParser} for include path options parsing. */ protected static class IncludePathOptionParser extends AbstractOptionParser { public IncludePathOptionParser(String pattern, String nameExpression) { super(ICLanguageSettingEntry.INCLUDE_PATH, pattern, nameExpression, nameExpression, 0); } public IncludePathOptionParser(String pattern, String nameExpression, int extraFlag) { super(ICLanguageSettingEntry.INCLUDE_PATH, pattern, nameExpression, nameExpression, extraFlag); } } /** * Implementation of {@link AbstractOptionParser} for include file options parsing. */ protected static class IncludeFileOptionParser extends AbstractOptionParser { /** * Constructor. * @param pattern - regular expression pattern being parsed by the parser. * @param nameExpression - capturing group expression defining name of an entry. */ public IncludeFileOptionParser(String pattern, String nameExpression) { super(ICLanguageSettingEntry.INCLUDE_FILE, pattern, nameExpression, nameExpression, 0); } /** * Constructor. * @param pattern - regular expression pattern being parsed by the parser. * @param nameExpression - capturing group expression defining name of an entry. * @param extraFlag - extra-flag to add while creating language settings entry. */ public IncludeFileOptionParser(String pattern, String nameExpression, int extraFlag) { super(ICLanguageSettingEntry.INCLUDE_FILE, pattern, nameExpression, nameExpression, extraFlag); } } /** * Implementation of {@link AbstractOptionParser} for macro options parsing. */ protected static class MacroOptionParser extends AbstractOptionParser { /** * Constructor. * @param pattern - regular expression pattern being parsed by the parser. * @param nameExpression - capturing group expression defining name of an entry. * @param valueExpression - capturing group expression defining value of an entry. */ public MacroOptionParser(String pattern, String nameExpression, String valueExpression) { super(ICLanguageSettingEntry.MACRO, pattern, nameExpression, valueExpression, 0); } /** * Constructor. * @param pattern - regular expression pattern being parsed by the parser. * @param nameExpression - capturing group expression defining name of an entry. * @param valueExpression - capturing group expression defining value of an entry. * @param extraFlag - extra-flag to add while creating language settings entry. */ public MacroOptionParser(String pattern, String nameExpression, String valueExpression, int extraFlag) { super(ICLanguageSettingEntry.MACRO, pattern, nameExpression, valueExpression, extraFlag); } /** * Constructor. * @param pattern - regular expression pattern being parsed by the parser. * @param nameExpression - capturing group expression defining name of an entry. * @param extraFlag - extra-flag to add while creating language settings entry. */ public MacroOptionParser(String pattern, String nameExpression, int extraFlag) { super(ICLanguageSettingEntry.MACRO, pattern, nameExpression, null, extraFlag); } } /** * Implementation of {@link AbstractOptionParser} for macro file options parsing. */ protected static class MacroFileOptionParser extends AbstractOptionParser { /** * Constructor. * @param pattern - regular expression pattern being parsed by the parser. * @param nameExpression - capturing group expression defining name of an entry. */ public MacroFileOptionParser(String pattern, String nameExpression) { super(ICLanguageSettingEntry.MACRO_FILE, pattern, nameExpression, nameExpression, 0); } /** * Constructor. * @param pattern - regular expression pattern being parsed by the parser. * @param nameExpression - capturing group expression defining name of an entry. * @param extraFlag - extra-flag to add while creating language settings entry. */ public MacroFileOptionParser(String pattern, String nameExpression, int extraFlag) { super(ICLanguageSettingEntry.MACRO_FILE, pattern, nameExpression, nameExpression, extraFlag); } } /** * Implementation of {@link AbstractOptionParser} for library path options parsing. */ protected static class LibraryPathOptionParser extends AbstractOptionParser { /** * Constructor. * @param pattern - regular expression pattern being parsed by the parser. * @param nameExpression - capturing group expression defining name of an entry. */ public LibraryPathOptionParser(String pattern, String nameExpression) { super(ICLanguageSettingEntry.LIBRARY_PATH, pattern, nameExpression, nameExpression, 0); } /** * Constructor. * @param pattern - regular expression pattern being parsed by the parser. * @param nameExpression - capturing group expression defining name of an entry. * @param extraFlag - extra-flag to add while creating language settings entry. */ public LibraryPathOptionParser(String pattern, String nameExpression, int extraFlag) { super(ICLanguageSettingEntry.LIBRARY_PATH, pattern, nameExpression, nameExpression, extraFlag); } } /** * Implementation of {@link AbstractOptionParser} for library file options parsing. */ protected static class LibraryFileOptionParser extends AbstractOptionParser { /** * Constructor. * @param pattern - regular expression pattern being parsed by the parser. * @param nameExpression - capturing group expression defining name of an entry. */ public LibraryFileOptionParser(String pattern, String nameExpression) { super(ICLanguageSettingEntry.LIBRARY_FILE, pattern, nameExpression, nameExpression, 0); } /** * Constructor. * @param pattern - regular expression pattern being parsed by the parser. * @param nameExpression - capturing group expression defining name of an entry. * @param extraFlag - extra-flag to add while creating language settings entry. */ public LibraryFileOptionParser(String pattern, String nameExpression, int extraFlag) { super(ICLanguageSettingEntry.LIBRARY_FILE, pattern, nameExpression, nameExpression, extraFlag); } } /** * Parse the line returning the resource name as appears in the output. * This is the resource where {@link ICLanguageSettingEntry} list is being added. * * @param line - one input line from the output stripped from end of line characters. * @return the resource name as appears in the output or {@code null}. * Note that {@code null} can have different semantics and can mean "no resource found" * or "applicable to any resource". By default "no resource found" is used in this * abstract class but extenders can handle otherwise. */ protected abstract String parseResourceName(String line); /** * Parse the line returning the list of substrings to be treated each as input to * the option parsers. It is assumed that each substring presents one * {@link ICLanguageSettingEntry} (for example compiler options {@code -I/path} or * {@code -DMACRO=1}). * * @param line - one input line from the output stripped from end of line characters. * @return list of substrings representing language settings entries. */ protected abstract List<String> parseOptions(String line); /** * @return array of option parsers defining how to parse a string to * {@link ICLanguageSettingEntry}. * See {@link AbstractOptionParser} and its specific extenders. */ protected abstract AbstractOptionParser[] getOptionParsers(); /** * @return {@code true} when the provider tries to resolve relative or remote paths * to the existing paths in the workspace or local file-system using certain heuristics. */ public boolean isResolvingPaths() { return isResolvingPaths; } /** * Enable or disable resolving relative or remote paths to the existing paths * in the workspace or local file-system. * * @param resolvePaths - set {@code true} to enable or {@code false} to disable * resolving paths. When this parameter is set to {@code false} the paths will * be kept as they appear in the build output. */ public void setResolvingPaths(boolean resolvePaths) { this.isResolvingPaths = resolvePaths; } @Override public void startup(ICConfigurationDescription cfgDescription, IWorkingDirectoryTracker cwdTracker) throws CoreException { this.currentCfgDescription = cfgDescription; this.currentProject = cfgDescription != null ? cfgDescription.getProjectDescription().getProject() : null; this.cwdTracker = cwdTracker; this.efsProvider = getEFSProvider(); } @Override public void shutdown() { // release resources for garbage collector // but keep currentCfgDescription for AbstractBuiltinSpecsDetector flow parsedResourceName = null; currentLanguageId = null; currentResource = null; cwdTracker = null; } @Override public boolean processLine(String line) { parsedResourceName = parseResourceName(line); currentResource = findResource(parsedResourceName); currentLanguageId = determineLanguage(); if (!isLanguageInScope(currentLanguageId)) { return false; } /** * URI of directory where the build is happening. This URI could point to a remote file-system * for remote builds. Most often it is the same file-system as for currentResource but * it can be different file-system (and different URI schema). */ URI buildDirURI = null; /** * Where source tree starts if mapped. This kind of mapping is useful for example in cases when * the absolute path to the source file on the remote system is simulated inside a project in the * workspace. * This URI is rooted on the same file-system where currentResource resides. In general this file-system * (or even URI schema) does not have to match that of buildDirURI. */ URI mappedRootURI = null; if (isResolvingPaths) { mappedRootURI = getMappedRootURI(currentResource, parsedResourceName); buildDirURI = getBuildDirURI(mappedRootURI); } List<ICLanguageSettingEntry> entries = new ArrayList<ICLanguageSettingEntry>(); List<String> options = parseOptions(line); if (options != null) { AbstractOptionParser[] optionParsers = getOptionParsers(); for (String option : options) { for (AbstractOptionParser optionParser : optionParsers) { try { if (optionParser.parseOption(option)) { ICLanguageSettingEntry entry = null; if (isResolvingPaths && (optionParser.isForFile() || optionParser.isForFolder())) { URI baseURI = mappedRootURI; if (buildDirURI != null && !new Path(optionParser.parsedName).isAbsolute()) { if (mappedRootURI != null) { baseURI = efsProvider.append(mappedRootURI, buildDirURI.getPath()); } else { baseURI = buildDirURI; } } entry = createResolvedPathEntry(optionParser, optionParser.parsedName, 0, baseURI); } else { entry = optionParser.createEntry(optionParser.parsedName, optionParser.parsedValue, 0); } if (entry != null && !entries.contains(entry)) { entries.add(entry); break; } } } catch (Throwable e) { @SuppressWarnings("nls") String msg = "Exception trying to parse option [" + option + "], class " + getClass().getSimpleName(); ManagedBuilderCorePlugin.log(new Status(IStatus.ERROR, ManagedBuilderCorePlugin.PLUGIN_ID, msg, e)); } } } if (entries.size() > 0) { setSettingEntries(entries); } else { setSettingEntries(null); } } return false; } /** * In case when absolute path is mapped to the source tree in a project * this function will try to figure mapping and return "mapped root", * i.e URI where the root path would be mapped. The mapped root will be * used to prepend to other "absolute" paths where appropriate. * * @param resource - a resource referred by parsed path * @param parsedResourceName - path as appears in the output * @return mapped path as URI */ protected URI getMappedRootURI(IResource resource, String parsedResourceName) { if (resource == null) { return null; } URI resourceURI = resource.getLocationURI(); String mappedRoot = "/"; //$NON-NLS-1$ if (parsedResourceName != null) { IPath parsedSrcPath = new Path(parsedResourceName); if (parsedSrcPath.isAbsolute()) { IPath absResourcePath = resource.getLocation(); int absSegmentsCount = absResourcePath.segmentCount(); int relSegmentsCount = parsedSrcPath.segmentCount(); if (absSegmentsCount >= relSegmentsCount) { IPath ending = absResourcePath.removeFirstSegments(absSegmentsCount - relSegmentsCount); ending = ending.setDevice(parsedSrcPath.getDevice()).makeAbsolute(); if (ending.equals(parsedSrcPath.makeAbsolute())) { // mappedRoot here is parsedSrcPath with removed parsedResourceName trailing segments, // i.e. if absResourcePath="/path/workspace/project/file.c" and parsedResourceName="project/file.c" // then mappedRoot="/path/workspace/" mappedRoot = absResourcePath.removeLastSegments(relSegmentsCount).toString(); } } } } // this creates URI with schema and other components from resourceURI but path as mappedRoot URI uri = efsProvider.createNewURIFromPath(resourceURI, mappedRoot); return uri; } /** * Determine current build directory considering currentResource (resource being compiled), * parsedResourceName and mappedRootURI. * * @param mappedRootURI - root of the source tree when mapped to remote file-system. * @return {@link URI} of current build directory */ protected URI getBuildDirURI(URI mappedRootURI) { URI buildDirURI = null; // try to deduce build directory from full path of currentResource and partial path of parsedResourceName URI cwdURI = null; if (currentResource != null && parsedResourceName != null && !new Path(parsedResourceName).isAbsolute()) { cwdURI = findBaseLocationURI(currentResource.getLocationURI(), parsedResourceName); } String cwdPath = cwdURI != null ? efsProvider.getPathFromURI(cwdURI) : null; if (cwdPath != null && mappedRootURI != null) { buildDirURI = efsProvider.append(mappedRootURI, cwdPath); } else { buildDirURI = cwdURI; } // try IWorkingDirectoryTracker if (buildDirURI == null && cwdTracker != null) { buildDirURI = cwdTracker.getWorkingDirectoryURI(); } // try builder working directory if (buildDirURI == null && currentCfgDescription != null) { IPath pathBuilderCWD = currentCfgDescription.getBuildSetting().getBuilderCWD(); if (pathBuilderCWD != null) { String builderCWD = pathBuilderCWD.toString(); try { // here is a hack to overcome ${workspace_loc:/prj-name} returned by builder // where "/" is treated as path separator by pathBuilderCWD ICdtVariableManager vmanager = CCorePlugin.getDefault().getCdtVariableManager(); builderCWD = vmanager.resolveValue(builderCWD, "", null, currentCfgDescription); //$NON-NLS-1$ } catch (CdtVariableException e) { ManagedBuilderCorePlugin.log(e); } if (builderCWD != null && !builderCWD.isEmpty()) { buildDirURI = org.eclipse.core.filesystem.URIUtil.toURI(builderCWD); } } } // try directory of the current project if (buildDirURI == null && currentProject != null) { buildDirURI = currentProject.getLocationURI(); } // try parent folder of the resource if (buildDirURI == null && currentResource != null) { IContainer container; if (currentResource instanceof IContainer) { container = (IContainer) currentResource; } else { container = currentResource.getParent(); } buildDirURI = container.getLocationURI(); } return buildDirURI; } /** * Sets language settings entries for current configuration description, current resource * and current language ID. * * @param entries - language settings entries to set. */ protected void setSettingEntries(List<? extends ICLanguageSettingEntry> entries) { setSettingEntries(currentCfgDescription, currentResource, currentLanguageId, entries); } /** * Determine a language associated with the resource. * * @return language ID for the resource. */ protected String determineLanguage() { IResource rc = currentResource; if (rc == null && currentProject != null && parsedResourceName != null) { String fileName = new Path(parsedResourceName).lastSegment().toString(); // use handle; resource does not need to exist rc = currentProject.getFile("__" + fileName); //$NON-NLS-1$ } if (rc == null) return null; List<String> languageIds = LanguageSettingsManager.getLanguages(rc, currentCfgDescription); if (languageIds.isEmpty()) return null; return languageIds.get(0); } /** * Determine if the language is in scope of the provider. * * @param languageId - language ID. * @return {@code true} if the language is in scope, {@code false } otherwise. */ protected boolean isLanguageInScope(String languageId) { List<String> languageIds = getLanguageScope(); return languageIds == null || languageIds.contains(languageId); } /** * Find file resource in the workspace for a given URI with a preference for the resource * to reside in the given project. */ private static IResource findFileForLocationURI(URI uri, IProject preferredProject, boolean checkExistence) { IResource sourceFile = null; IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IResource[] resources = root.findFilesForLocationURI(uri); for (IResource rc : resources) { if (!checkExistence || rc.isAccessible()) { if (rc.getProject().equals(preferredProject)) { sourceFile = rc; break; } if (sourceFile == null) { sourceFile = rc; } } } return sourceFile; } /** * Return a resource in workspace corresponding the given folder {@link URI} preferable residing in * the provided project. */ private static IResource findContainerForLocationURI(URI uri, IProject preferredProject, boolean checkExistence) { IResource resource = null; IResource[] resources = ResourcesPlugin.getWorkspace().getRoot().findContainersForLocationURI(uri); for (IResource rc : resources) { if ((rc instanceof IProject || rc instanceof IFolder) && (!checkExistence || rc.isAccessible())) { // treat IWorkspaceRoot as non-workspace path if (rc.getProject().equals(preferredProject)) { resource = rc; break; } if (resource == null) { resource = rc; // to be deterministic the first qualified resource has preference } } } return resource; } /** * Determine resource in the workspace corresponding to the parsed resource name. */ private IResource findResource(String parsedResourceName) { if (parsedResourceName == null || parsedResourceName.isEmpty()) { return null; } IResource sourceFile = null; // try ErrorParserManager if (cwdTracker instanceof ErrorParserManager) { sourceFile = ((ErrorParserManager) cwdTracker).findFileName(parsedResourceName); } // try to find absolute path in the workspace if (sourceFile == null && new Path(parsedResourceName).isAbsolute()) { URI uri = org.eclipse.core.filesystem.URIUtil.toURI(parsedResourceName); sourceFile = findFileForLocationURI(uri, currentProject, /*checkExistence*/ true); } // try last known current working directory from build output if (sourceFile == null && cwdTracker != null) { URI cwdURI = cwdTracker.getWorkingDirectoryURI(); if (cwdURI != null) { URI uri = efsProvider.append(cwdURI, parsedResourceName); sourceFile = findFileForLocationURI(uri, currentProject, /*checkExistence*/ true); } } // try path relative to build dir from configuration if (sourceFile == null && currentCfgDescription != null) { IPath builderCWD = currentCfgDescription.getBuildSetting().getBuilderCWD(); if (builderCWD != null) { String strBuilderCWD = builderCWD.toString(); try { ICdtVariableManager varManager = CCorePlugin.getDefault().getCdtVariableManager(); strBuilderCWD = varManager.resolveValue(strBuilderCWD, "", null, currentCfgDescription); //$NON-NLS-1$ } catch (Exception e) { @SuppressWarnings("nls") String msg = "Exception trying to resolve value [" + strBuilderCWD + "]"; ManagedBuilderCorePlugin.log(new Status(IStatus.ERROR, ManagedBuilderCorePlugin.PLUGIN_ID, msg, e)); } builderCWD = new Path(strBuilderCWD); IPath path = builderCWD.append(parsedResourceName); URI uri = org.eclipse.core.filesystem.URIUtil.toURI(path); sourceFile = findFileForLocationURI(uri, currentProject, /*checkExistence*/ true); } } // try path relative to the project if (sourceFile == null && currentProject != null) { sourceFile = currentProject.findMember(parsedResourceName); } return sourceFile; } /** * Find base location of the file, i.e. location of the directory which * results from removing trailing relativeFileName from fileURI or * {@code null} if fileURI doesn't represent relativeFileName. */ private static URI findBaseLocationURI(URI fileURI, String relativeFileName) { URI cwdURI = null; String path = fileURI.getPath(); String[] segments = relativeFileName.split("[/\\\\]"); //$NON-NLS-1$ // start removing segments from the end of the path for (int i = segments.length - 1; i >= 0; i--) { String lastSegment = segments[i]; if (lastSegment.length() > 0 && !lastSegment.equals(".")) { //$NON-NLS-1$ if (lastSegment.equals("..")) { //$NON-NLS-1$ // navigating ".." in the other direction is ambiguous, bailing out return null; } else { if (path.endsWith("/" + lastSegment)) { //$NON-NLS-1$ int pos = path.lastIndexOf("/" + lastSegment); //$NON-NLS-1$ path = path.substring(0, pos); continue; } else { // ouch, relativeFileName does not match fileURI, bailing out return null; } } } } try { cwdURI = new URI(fileURI.getScheme(), fileURI.getUserInfo(), fileURI.getHost(), fileURI.getPort(), path + '/', fileURI.getQuery(), fileURI.getFragment()); } catch (URISyntaxException e) { // It should be valid URI here or something is really wrong ManagedBuilderCorePlugin.log(e); } return cwdURI; } /** * The manipulations here are done to resolve problems such as "../" navigation for symbolic links where * "link/.." cannot be collapsed as it must follow the real file-system path. {@link java.io.File#getCanonicalPath()} * deals with that correctly but {@link Path} or {@link URI} try to normalize the path which would be incorrect here. * Another issue being resolved here is fixing drive letters in URI syntax. */ private static URI resolvePathFromBaseLocation(String pathStr0, IPath baseLocation) { String pathStr = pathStr0; if (baseLocation != null && !baseLocation.isEmpty()) { pathStr = pathStr.replace(File.separatorChar, '/'); String device = new Path(pathStr).getDevice(); if (device == null || device.equals(baseLocation.getDevice())) { if (device != null && device.length() > 0) { pathStr = pathStr.substring(device.length()); } baseLocation = baseLocation.addTrailingSeparator(); if (pathStr.startsWith("/")) { //$NON-NLS-1$ pathStr = pathStr.substring(1); } pathStr = baseLocation.toString() + pathStr; } } try { File file = new File(pathStr); file = file.getCanonicalFile(); URI uri = file.toURI(); if (file.exists()) { return uri; } IPath path0 = new Path(pathStr0); if (!path0.isAbsolute()) { return uri; } String device = path0.getDevice(); if (device == null || device.isEmpty()) { // Avoid spurious adding of drive letters on Windows pathStr = path0.setDevice(null).toString(); } else { // On Windows "C:/folder/" -> "/C:/folder/" if (pathStr.charAt(0) != IPath.SEPARATOR) { pathStr = IPath.SEPARATOR + pathStr; } } return new URI(uri.getScheme(), uri.getAuthority(), pathStr, uri.getQuery(), uri.getFragment()); } catch (Exception e) { // if error will leave it as is ManagedBuilderCorePlugin.log(e); } return org.eclipse.core.filesystem.URIUtil.toURI(pathStr); } /** * Determine URI on the local file-system considering possible mapping. * * @param pathStr - path to the resource, can be absolute or relative * @param baseURI - base {@link URI} where path to the resource is rooted * @return {@link URI} of the resource */ private URI determineMappedURI(String pathStr, URI baseURI) { URI uri = null; if (baseURI == null) { if (new Path(pathStr).isAbsolute()) { uri = resolvePathFromBaseLocation(pathStr, Path.ROOT); } } else if (baseURI.getScheme().equals(EFS.SCHEME_FILE)) { // location on the local file-system IPath baseLocation = org.eclipse.core.filesystem.URIUtil.toPath(baseURI); // careful not to use Path here but 'pathStr' as String as we want to properly navigate symlinks uri = resolvePathFromBaseLocation(pathStr, baseLocation); } else { // location on a remote file-system IPath path = new Path(pathStr); // use canonicalized path here, in particular replace all '\' with '/' for Windows paths URI remoteUri = efsProvider.append(baseURI, path.toString()); if (remoteUri != null) { String localPath = efsProvider.getMappedPath(remoteUri); if (localPath != null) { uri = org.eclipse.core.filesystem.URIUtil.toURI(localPath); } } } if (uri == null) { // if everything fails just wrap string to URI uri = org.eclipse.core.filesystem.URIUtil.toURI(pathStr); } return uri; } /** * Find all resources in the folder which might be represented by relative path passed. */ private static List<IResource> findPathInFolder(IPath path, IContainer folder) { List<IResource> paths = new ArrayList<IResource>(); IResource resource = folder.findMember(path); if (resource != null) { paths.add(resource); } try { for (IResource res : folder.members()) { if (res instanceof IContainer) { paths.addAll(findPathInFolder(path, (IContainer) res)); } } } catch (CoreException e) { // ignore } return paths; } /** * Determine which resource in workspace is the best fit to parsedName passed. */ private IResource findBestFitInWorkspace(String parsedName) { Set<String> referencedProjectsNames = new LinkedHashSet<String>(); if (currentCfgDescription != null) { Map<String,String> refs = currentCfgDescription.getReferenceInfo(); referencedProjectsNames.addAll(refs.keySet()); } IPath path = new Path(parsedName); if (path.equals(new Path(".")) || path.equals(new Path(".."))) { //$NON-NLS-1$ //$NON-NLS-2$ return null; } // prefer current project if (currentProject != null) { List<IResource> result = findPathInFolder(path, currentProject); int size = result.size(); if (size == 1) { // found the one return result.get(0); } else if (size > 1) { // ambiguous return null; } } IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); // then prefer referenced projects if (referencedProjectsNames.size() > 0) { IResource rc = null; for (String prjName : referencedProjectsNames) { IProject prj = root.getProject(prjName); if (prj.isOpen()) { List<IResource> result = findPathInFolder(path, prj); int size = result.size(); if (size == 1 && rc == null) { rc = result.get(0); } else if (size > 0) { // ambiguous rc = null; break; } } } if (rc != null) { return rc; } } // then check all other projects in workspace IProject[] projects = root.getProjects(); if (projects.length > 0) { IResource rc = null; for (IProject prj : projects) { if (!prj.equals(currentProject) && !referencedProjectsNames.contains(prj.getName()) && prj.isOpen()) { List<IResource> result = findPathInFolder(path, prj); int size = result.size(); if (size == 1 && rc == null) { rc = result.get(0); } else if (size > 0) { // ambiguous rc = null; break; } } } if (rc != null) { return rc; } } // not found or ambiguous return null; } /** * Get location on the local file-system considering possible mapping by EFS provider. See {@link EFSExtensionManager}. */ private IPath getFilesystemLocation(URI uri) { if (uri == null) return null; String pathStr = efsProvider.getMappedPath(uri); uri = org.eclipse.core.filesystem.URIUtil.toURI(pathStr); if (uri != null && uri.isAbsolute()) { try { File file = new java.io.File(uri); String canonicalPathStr = file.getCanonicalPath(); if (new Path(pathStr).getDevice() == null) { return new Path(canonicalPathStr).setDevice(null); } return new Path(canonicalPathStr); } catch (Exception e) { ManagedBuilderCorePlugin.log(e); } } return null; } /** * Resolve and create language settings path entry. */ private ICLanguageSettingEntry createResolvedPathEntry(AbstractOptionParser optionParser, String parsedPath, int flag, URI baseURI) { URI uri = determineMappedURI(parsedPath, baseURI); boolean isRelative = !new Path(parsedPath).isAbsolute(); // is mapped something that is not a project root boolean isRemapped = baseURI != null && currentProject != null && !baseURI.equals(currentProject.getLocationURI()); boolean presentAsRelative = isRelative || isRemapped; ICLanguageSettingEntry entry = resolvePathEntryInWorkspace(optionParser, uri, flag, presentAsRelative); if (entry != null) { return entry; } entry = resolvePathEntryInFilesystem(optionParser, uri, flag); if (entry != null) { return entry; } entry = resolvePathEntryInWorkspaceAsBestFit(optionParser, parsedPath, flag, presentAsRelative); if (entry != null) { return entry; } entry = resolvePathEntryInWorkspaceToNonexistingResource(optionParser, uri, flag, presentAsRelative); if (entry != null) { return entry; } entry = resolvePathEntryInFilesystemToNonExistingResource(optionParser, uri, flag); if (entry != null) { return entry; } return optionParser.createEntry(parsedPath, parsedPath, flag); } /** * Create a language settings entry for a given resource. * This will represent relative path using CDT variable ${ProjName}. */ private ICLanguageSettingEntry createPathEntry(AbstractOptionParser optionParser, IResource rc, boolean isRelative, int flag) { String path; if (isRelative && rc.getProject().equals(currentProject)) { path = PROJ_NAME_PREFIX + rc.getFullPath().removeFirstSegments(1); flag = flag | ICSettingEntry.VALUE_WORKSPACE_PATH; } else { path = rc.getFullPath().toString(); flag = flag | ICSettingEntry.VALUE_WORKSPACE_PATH | ICSettingEntry.RESOLVED; } return optionParser.createEntry(path, path, flag); } /** * Find an existing resource in the workspace and create a language settings entry for it. */ private ICLanguageSettingEntry resolvePathEntryInWorkspace(AbstractOptionParser optionParser, URI uri, int flag, boolean isRelative) { if (uri != null && uri.isAbsolute()) { IResource rc = null; if (optionParser.isForFolder()) { rc = findContainerForLocationURI(uri, currentProject, /*checkExistence*/ true); } else if (optionParser.isForFile()) { rc = findFileForLocationURI(uri, currentProject, /*checkExistence*/ true); } if (rc != null) { return createPathEntry(optionParser, rc, isRelative, flag); } } return null; } /** * Find a resource on the file-system and create a language settings entry for it. */ private ICLanguageSettingEntry resolvePathEntryInFilesystem(AbstractOptionParser optionParser, URI uri, int flag) { IPath location = getFilesystemLocation(uri); if (location != null) { String loc = location.toString(); if (new File(loc).exists()) { return optionParser.createEntry(loc, loc, flag); } } return null; } /** * Find a best fit for the resource in the workspace and create a language settings entry for it. */ private ICLanguageSettingEntry resolvePathEntryInWorkspaceAsBestFit(AbstractOptionParser optionParser, String parsedPath, int flag, boolean isRelative) { IResource rc = findBestFitInWorkspace(parsedPath); if (rc != null) { return createPathEntry(optionParser, rc, isRelative, flag); } return null; } /** * Try to map a resource in the workspace even if it does not exist and create a language settings entry for it. */ private ICLanguageSettingEntry resolvePathEntryInWorkspaceToNonexistingResource(AbstractOptionParser optionParser, URI uri, int flag, boolean isRelative) { if (uri != null && uri.isAbsolute()) { IResource rc = null; if (optionParser.isForFolder()) { rc = findContainerForLocationURI(uri, currentProject, /*checkExistence*/ false); } else if (optionParser.isForFile()) { rc = findFileForLocationURI(uri, currentProject, /*checkExistence*/ false); } if (rc != null) { return createPathEntry(optionParser, rc, isRelative, flag); } } return null; } /** * Try to map a resource on the file-system even if it does not exist and create a language settings entry for it. */ private ICLanguageSettingEntry resolvePathEntryInFilesystemToNonExistingResource(AbstractOptionParser optionParser, URI uri, int flag) { IPath location = getFilesystemLocation(uri); if (location != null) { return optionParser.createEntry(location.toString(), location.toString(), flag); } return null; } /** * Count how many groups are present in regular expression. * The implementation is simplistic but should be sufficient for the cause. * * @param str - regular expression to count the groups. * @return number of the groups (groups are enclosed in round brackets) present. */ protected static int countGroups(String str) { @SuppressWarnings("nls") int count = str.replaceAll("[^\\(]", "").length(); return count; } /** * Helper method to construct logical "or" to be used inside regular expressions. */ @SuppressWarnings("nls") private static String expressionLogicalOr(Set<String> fileExts) { String pattern = "("; for (String ext : fileExts) { if (pattern.length() != 1) pattern += "|"; pattern += "(" + Pattern.quote(ext) + ")"; ext = ext.toUpperCase(); if (!fileExts.contains(ext)) { pattern += "|(" + Pattern.quote(ext) + ")"; } } pattern += ")"; return pattern; } /** * Construct regular expression to find any file extension for C or C++. * Returns expression shaped in form of "((cpp)|(c++)|(c))". * * @return regular expression for searching C/C++ file extensions. */ protected String getPatternFileExtensions() { IContentTypeManager manager = Platform.getContentTypeManager(); Set<String> fileExts = new HashSet<String>(); IContentType contentTypeCpp = manager.getContentType(CCorePlugin.CONTENT_TYPE_CXXSOURCE); fileExts.addAll(Arrays.asList(contentTypeCpp.getFileSpecs(IContentType.FILE_EXTENSION_SPEC))); IContentType contentTypeC = manager.getContentType(CCorePlugin.CONTENT_TYPE_CSOURCE); fileExts.addAll(Arrays.asList(contentTypeC.getFileSpecs(IContentType.FILE_EXTENSION_SPEC))); String pattern = expressionLogicalOr(fileExts); return pattern; } /** * This {@link EFSExtensionProvider} is capable to translate EFS paths to and from local * file-system. Added mostly for Cygwin translations. * * This usage of {@link EFSExtensionProvider} is somewhat a misnomer. This provider is not * an "extension" provider but rather a wrapper on {@link EFSExtensionManager} which in fact * will use genuine {@link EFSExtensionProvider}s defined as extensions. * * @since 8.2 */ protected EFSExtensionProvider getEFSProvider() { return efsProviderDefault; } @Override public Element serializeAttributes(Element parentElement) { Element elementProvider = super.serializeAttributes(parentElement); elementProvider.setAttribute(ATTR_KEEP_RELATIVE_PATHS, Boolean.toString( ! isResolvingPaths )); return elementProvider; } @Override public void loadAttributes(Element providerNode) { super.loadAttributes(providerNode); String expandRelativePathsValue = XmlUtil.determineAttributeValue(providerNode, ATTR_KEEP_RELATIVE_PATHS); if (expandRelativePathsValue!=null) isResolvingPaths = ! Boolean.parseBoolean(expandRelativePathsValue); } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + (isResolvingPaths ? 1231 : 1237); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!super.equals(obj)) return false; if (getClass() != obj.getClass()) return false; AbstractLanguageSettingsOutputScanner other = (AbstractLanguageSettingsOutputScanner) obj; if (isResolvingPaths != other.isResolvingPaths) return false; return true; } }