blob: f9996d700367641e380b7600de9a33fbec1f1356 [file] [log] [blame]
/*
* Copyright (c) 2013, 2015 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.internal.qt.core;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.eclipse.cdt.core.language.settings.providers.LanguageSettingsSerializableProvider;
import org.eclipse.cdt.core.settings.model.CIncludePathEntry;
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.utils.spawner.ProcessFactory;
import org.eclipse.core.resources.IResource;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
* Discovers and persists the list of Qt include paths for a particular installation of
* Qt. The Qt installation is described by the path to qmake.
* <p>
* Qt uses a tool called qmake to generate makefiles for Qt projects. The tool has a
* query mode that can be used to discover information about the Qt installation. Here
* qmake is used to build a list of all installed Qt include paths.
* <p>
* These paths are persisted into a file called language-settings.xml in the workspace
* metadata area.
*
* @see QtIncludePathsProvider
*/
public class QtIncludePaths extends LanguageSettingsSerializableProvider {
/**
* The path to the qmake executable uniquely identifies this installation.
*/
private final String qmakePath;
/**
* The cached data is reloaded when the qmake executable is modified.
*/
private long qmakeModTime;
/**
* The cached data is reloaded when the folder holding the include paths
* is removed.
*/
private String qtInstallHeadersPath;
/**
* The cached data is reloaded when the folder containing the include folders is
* modified.
*/
private long qtInstallHeadersModTime;
private static final String ATTR_QMAKE = "qmake"; //$NON-NLS-1$
private static final String ATTR_QMAKE_MOD = "qmakeModification"; //$NON-NLS-1$
private static final String ATTR_QT_INSTALL_HEADERS = "QT_INSTALL_HEADERS"; //$NON-NLS-1$
private static final String ATTR_QT_INSTALL_HEADERS_MOD = "qtInstallHeadersModification"; //$NON-NLS-1$
/**
* Create a new instance of the include path wrapper for the Qt installation for
* the given qmake binary.
*/
public QtIncludePaths(String qmakePath) {
this.qmakePath = qmakePath;
}
/**
* Create and load an instance of QtIncludePaths from data that was serialized into the
* given XML element. Return null if an instance cannot be loaded or if the installation
* is no longer valid.
*/
public static QtIncludePaths loadFrom(Node node) {
if (node.getNodeType() != Node.ELEMENT_NODE)
return null;
Element element = (Element) node;
String qmakePath = element.getAttribute(ATTR_QMAKE);
if (qmakePath == null || qmakePath.isEmpty())
return null;
QtIncludePaths qtIncludePaths = new QtIncludePaths(qmakePath);
qtIncludePaths.load(element);
return qtIncludePaths;
}
public String getQMakePath() {
return qmakePath;
}
/**
* Return true if the receiver points to a valid Qt installation and false otherwise.
* The installation is considered valid if an executable qmake binary exists at the
* expected location.
*/
public boolean isValid() {
if (qmakePath == null || qmakePath.isEmpty())
return false;
File qmake = new File(qmakePath);
return qmake.exists() && qmake.canExecute();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof QtIncludePaths))
return super.equals(obj);
// Include paths are equivalent when they point to the same qmake binary. All other
// values are reloaded from that binary and do not need to be directly compared.
QtIncludePaths other = (QtIncludePaths) obj;
return qmakePath == null ? other.qmakePath == null : qmakePath.equals(other.qmakePath);
}
@Override
public int hashCode() {
return qmakePath == null ? 0 : qmakePath.hashCode();
}
/**
* Return a current list of the include paths for this Qt installation. Return null if
* no such paths can be found.
* <p>
* Updates the cached results if needed. If the settings are updated then the new list
* will be serialized into the workspace metadata area.
*/
@Override
public List<ICLanguageSettingEntry> getSettingEntries(ICConfigurationDescription configDesc, IResource rc,
String languageId) {
List<ICLanguageSettingEntry> entries = null;
File qmake = new File(qmakePath);
if (!qmake.exists() || qmakeModTime != qmake.lastModified())
entries = reload();
else {
File qtInstallHeadersDir = new File(qtInstallHeadersPath);
if (!qtInstallHeadersDir.exists() || qtInstallHeadersModTime != qtInstallHeadersDir.lastModified())
entries = reload();
}
// If the cache was not reloaded, then return the previously discovered entries.
if (entries == null)
return super.getSettingEntries(configDesc, rc, languageId);
// Otherwise store, persist, and return the newly discovered values.
setSettingEntries(configDesc, rc, languageId, entries);
serializeLanguageSettingsInBackground(null);
return entries;
}
@Override
public Element serializeAttributes(Element parentElement) {
parentElement.setAttribute(ATTR_QMAKE, qmakePath);
parentElement.setAttribute(ATTR_QMAKE_MOD, Long.toString(qmakeModTime));
parentElement.setAttribute(ATTR_QT_INSTALL_HEADERS, qtInstallHeadersPath);
parentElement.setAttribute(ATTR_QT_INSTALL_HEADERS_MOD, Long.toString(qtInstallHeadersModTime));
// The parent implementation tries to create a new child node (provider) that is used
// as the part for later entries. This isn't needed in this case, we just want to
// use the part that serializes the languages.
return parentElement;
}
@Override
public void loadAttributes(Element element) {
qmakeModTime = getLongAttribute(element, ATTR_QMAKE_MOD);
qtInstallHeadersPath = element.getAttribute(ATTR_QT_INSTALL_HEADERS);
qtInstallHeadersModTime = getLongAttribute(element, ATTR_QT_INSTALL_HEADERS_MOD);
// The parent implementation tries to create a new child node (provider) that is used
// as the part for later entries. This isn't needed in this case, we just want to
// use the part that serializes the languages.
}
/**
* Parse and return the given attribute as a long. Return 0 if the attribute does
* not have a valid value.
*/
private static long getLongAttribute(Element element, String attr) {
String value = element.getAttribute(attr);
if (value == null || value.isEmpty())
return 0;
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
Activator.log("attribute name:" + attr + " value:" + value, e); //$NON-NLS-1$ //$NON-NLS-2$
return 0;
}
}
/**
* Reload and return the entries if possible, return null otherwise.
*/
private List<ICLanguageSettingEntry> reload() {
// All keys are reset and then updated as their values are discovered. This allows partial
// success to skip over previously calculated values.
qmakeModTime = 0;
qtInstallHeadersPath = null;
qtInstallHeadersModTime = 0;
File qmake = new File(qmakePath);
if (!qmake.exists() || !qmake.canExecute())
return Collections.emptyList();
qmakeModTime = qmake.lastModified();
// Run `qmake -query QT_INSTALL_HEADERS` to get output like "/opt/qt-5.0.0/include".
BufferedReader reader = null;
Process process = null;
try {
process = ProcessFactory.getFactory().exec(new String[] { qmakePath, "-query", "QT_INSTALL_HEADERS" }); //$NON-NLS-1$ //$NON-NLS-2$
reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
qtInstallHeadersPath = reader.readLine();
} catch (IOException e) {
Activator.log(e);
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
/* ignore */
} finally {
if (process != null)
process.destroy();
}
}
if (qtInstallHeadersPath == null)
return Collections.emptyList();
File qtInstallHeadersDir = new File(qtInstallHeadersPath);
qtInstallHeadersModTime = qtInstallHeadersDir.lastModified();
if (!qtInstallHeadersDir.exists() || !qtInstallHeadersDir.canRead() || !qtInstallHeadersDir.isDirectory())
return Collections.emptyList();
// Create an include path entry for all sub-folders in the QT_INSTALL_HEADERS location, including
// the QT_INSTALL_HEADERS folder itself.
File[] files = qtInstallHeadersDir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.exists() && pathname.isDirectory();
}
});
List<ICLanguageSettingEntry> entries = new ArrayList<>(files.length + 1);
safeAdd(entries, qtInstallHeadersDir);
for (File file : files)
safeAdd(entries, file);
return entries;
}
private static void safeAdd(List<ICLanguageSettingEntry> entries, File file) {
try {
entries.add(
new CIncludePathEntry(file.getCanonicalPath(), ICSettingEntry.READONLY | ICSettingEntry.RESOLVED));
} catch (IOException e) {
Activator.log(e);
}
}
}