ProjectExplorer: Report project-specific kit errors/warnings

Report project-specific warnings about the kit used in Project Mode.
E.g. a python project should not complain about missing toolchains,
while a qmake project should.

Change-Id: I5ce6742683cdeffc7ff3f1a3e8f0b89aee9aa0b4
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Tobias Hunger
2018-04-18 13:39:05 +02:00
parent da18fc1f50
commit 460fdc02e0
17 changed files with 119 additions and 105 deletions

View File

@@ -363,14 +363,16 @@ bool CMakeProject::knowsAllBuildExecutables() const
return false; return false;
} }
bool CMakeProject::supportsKit(const Kit *k, QString *errorMessage) const QList<Task> CMakeProject::projectIssues(const Kit *k) const
{ {
if (!CMakeKitInformation::cmakeTool(k)) { QList<Task> result = Project::projectIssues(k);
if (errorMessage)
*errorMessage = tr("No cmake tool set."); if (!CMakeKitInformation::cmakeTool(k))
return false; result.append(createProjectTask(Task::TaskType::Error, tr("No cmake tool set.")));
} if (ToolChainKitInformation::toolChains(k).isEmpty())
return true; result.append(createProjectTask(Task::TaskType::Warning, tr("No compilers set in kit.")));
return result;
} }
void CMakeProject::runCMake() void CMakeProject::runCMake()

View File

@@ -67,7 +67,7 @@ public:
bool knowsAllBuildExecutables() const final; bool knowsAllBuildExecutables() const final;
bool supportsKit(const ProjectExplorer::Kit *k, QString *errorMessage = nullptr) const final; QList<ProjectExplorer::Task> projectIssues(const ProjectExplorer::Kit *k) const final;
void runCMake(); void runCMake();
void runCMakeAndScanProjectTree(); void runCMakeAndScanProjectTree();

View File

@@ -154,20 +154,16 @@ void NimProject::updateProject()
emitParsingFinished(true); emitParsingFinished(true);
} }
bool NimProject::supportsKit(const Kit *k, QString *errorMessage) const QList<Task> NimProject::projectIssues(const Kit *k) const
{ {
QList<Task> result = Project::projectIssues(k);
auto tc = dynamic_cast<NimToolChain*>(ToolChainKitInformation::toolChain(k, Constants::C_NIMLANGUAGE_ID)); auto tc = dynamic_cast<NimToolChain*>(ToolChainKitInformation::toolChain(k, Constants::C_NIMLANGUAGE_ID));
if (!tc) { if (!tc)
if (errorMessage) result.append(createProjectTask(Task::TaskType::Error, tr("No Nim compiler set.")));
*errorMessage = tr("No Nim compiler set."); if (!tc->compilerCommand().exists())
return false; result.append(createProjectTask(Task::TaskType::Error, tr("Nim compiler does not exist.")));
}
if (!tc->compilerCommand().exists()) { return result;
if (errorMessage)
*errorMessage = tr("Nim compiler does not exist.");
return false;
}
return true;
} }
FileNameList NimProject::nimFiles() const FileNameList NimProject::nimFiles() const

View File

@@ -41,7 +41,7 @@ class NimProject : public ProjectExplorer::Project
public: public:
explicit NimProject(const Utils::FileName &fileName); explicit NimProject(const Utils::FileName &fileName);
bool supportsKit(const ProjectExplorer::Kit *k, QString *errorMessage) const final; QList<ProjectExplorer::Task> projectIssues(const ProjectExplorer::Kit *k) const final;
Utils::FileNameList nimFiles() const; Utils::FileNameList nimFiles() const;
QVariantMap toMap() const final; QVariantMap toMap() const final;

View File

@@ -407,7 +407,7 @@ bool IBuildConfigurationFactory::canHandle(const Target *target) const
if (m_supportedProjectType.isValid() && m_supportedProjectType != target->project()->id()) if (m_supportedProjectType.isValid() && m_supportedProjectType != target->project()->id())
return false; return false;
if (!target->project()->supportsKit(target->kit())) if (containsType(target->project()->projectIssues(target->kit()), Task::TaskType::Error))
return false; return false;
if (!supportsTargetDeviceType(DeviceTypeKitInformation::deviceTypeId(target->kit()))) if (!supportsTargetDeviceType(DeviceTypeKitInformation::deviceTypeId(target->kit())))

View File

@@ -184,7 +184,7 @@ bool DeployConfigurationFactory::canHandle(Target *target) const
return false; return false;
} }
if (!target->project()->supportsKit(target->kit())) if (containsType(target->project()->projectIssues(target->kit()), Task::TaskType::Error))
return false; return false;
if (!m_supportedTargetDeviceTypes.isEmpty()) { if (!m_supportedTargetDeviceTypes.isEmpty()) {

View File

@@ -112,7 +112,7 @@ public:
int m_nestedBlockingLevel = 0; int m_nestedBlockingLevel = 0;
bool m_autodetected = false; bool m_autodetected = false;
bool m_sdkProvided = false; bool m_sdkProvided = false;
bool m_isValid = true; bool m_hasError = false;
bool m_hasWarning = false; bool m_hasWarning = false;
bool m_hasValidityInfo = false; bool m_hasValidityInfo = false;
bool m_mustNotify = false; bool m_mustNotify = false;
@@ -209,7 +209,7 @@ Kit *Kit::clone(bool keepName) const
k->d->m_autodetected = false; k->d->m_autodetected = false;
k->d->m_data = d->m_data; k->d->m_data = d->m_data;
// Do not clone m_fileSystemFriendlyName, needs to be unique // Do not clone m_fileSystemFriendlyName, needs to be unique
k->d->m_isValid = d->m_isValid; k->d->m_hasError = d->m_hasError;
k->d->m_cachedIcon = d->m_cachedIcon; k->d->m_cachedIcon = d->m_cachedIcon;
k->d->m_iconPath = d->m_iconPath; k->d->m_iconPath = d->m_iconPath;
k->d->m_sticky = d->m_sticky; k->d->m_sticky = d->m_sticky;
@@ -240,7 +240,7 @@ bool Kit::isValid() const
if (!d->m_hasValidityInfo) if (!d->m_hasValidityInfo)
validate(); validate();
return d->m_isValid; return !d->m_hasError;
} }
bool Kit::hasWarning() const bool Kit::hasWarning() const
@@ -255,18 +255,13 @@ QList<Task> Kit::validate() const
{ {
QList<Task> result; QList<Task> result;
QList<KitInformation *> infoList = KitManager::kitInformation(); QList<KitInformation *> infoList = KitManager::kitInformation();
d->m_isValid = true; for (KitInformation *i : infoList) {
d->m_hasWarning = false;
foreach (KitInformation *i, infoList) {
QList<Task> tmp = i->validate(this); QList<Task> tmp = i->validate(this);
foreach (const Task &t, tmp) {
if (t.type == Task::Error)
d->m_isValid = false;
if (t.type == Task::Warning)
d->m_hasWarning = true;
}
result.append(tmp); result.append(tmp);
} }
d->m_hasError = containsType(result, Task::TaskType::Error);
d->m_hasWarning = containsType(result, Task::TaskType::Warning);
Utils::sort(result); Utils::sort(result);
d->m_hasValidityInfo = true; d->m_hasValidityInfo = true;
return result; return result;

View File

@@ -198,7 +198,9 @@ Project::Project(const QString &mimeType, const Utils::FileName &fileName,
// Only set up containernode after d is set so that it will find the project directory! // Only set up containernode after d is set so that it will find the project directory!
d->m_containerNode = std::make_unique<ContainerNode>(this); d->m_containerNode = std::make_unique<ContainerNode>(this);
setRequiredKitPredicate([this](const Kit *k) { return supportsKit(k); }); setRequiredKitPredicate([this](const Kit *k) {
return !containsType(projectIssues(k), Task::TaskType::Error);
});
} }
Project::~Project() Project::~Project()
@@ -317,11 +319,12 @@ Target *Project::target(Kit *k) const
return Utils::findOrDefault(d->m_targets, Utils::equal(&Target::kit, k)); return Utils::findOrDefault(d->m_targets, Utils::equal(&Target::kit, k));
} }
bool Project::supportsKit(const Kit *k, QString *errorMessage) const QList<Task> Project::projectIssues(const Kit *k) const
{ {
Q_UNUSED(k); QList<Task> result;
Q_UNUSED(errorMessage); if (!k->isValid())
return true; result.append(createProjectTask(Task::TaskType::Error, tr("Kit is not valid.")));
return {};
} }
Target *Project::createTarget(Kit *k) Target *Project::createTarget(Kit *k)
@@ -762,6 +765,11 @@ void Project::projectLoaded()
{ {
} }
Task Project::createProjectTask(Task::TaskType type, const QString &description)
{
return Task(type, description, Utils::FileName(), -1, Core::Id());
}
Core::Context Project::projectContext() const Core::Context Project::projectContext() const
{ {
return Core::Context(d->m_id); return Core::Context(d->m_id);

View File

@@ -122,7 +122,7 @@ public:
Target *activeTarget() const; Target *activeTarget() const;
Target *target(Core::Id id) const; Target *target(Core::Id id) const;
Target *target(Kit *k) const; Target *target(Kit *k) const;
virtual bool supportsKit(const Kit *k, QString *errorMessage = nullptr) const; virtual QList<Task> projectIssues(const Kit *k) const;
Target *createTarget(Kit *k); Target *createTarget(Kit *k);
static bool copySteps(Target *sourceTarget, Target *newTarget); static bool copySteps(Target *sourceTarget, Target *newTarget);
@@ -238,6 +238,9 @@ protected:
void setProjectLanguage(Core::Id id, bool enabled); void setProjectLanguage(Core::Id id, bool enabled);
virtual void projectLoaded(); // Called when the project is fully loaded. virtual void projectLoaded(); // Called when the project is fully loaded.
static ProjectExplorer::Task createProjectTask(ProjectExplorer::Task::TaskType type,
const QString &description);
private: private:
// The predicate used to select kits available in TargetSetupPage. // The predicate used to select kits available in TargetSetupPage.
void setRequiredKitPredicate(const Kit::Predicate &predicate); void setRequiredKitPredicate(const Kit::Predicate &predicate);

View File

@@ -540,7 +540,7 @@ bool RunConfigurationFactory::canHandle(Target *target) const
const Project *project = target->project(); const Project *project = target->project();
Kit *kit = target->kit(); Kit *kit = target->kit();
if (!project->supportsKit(kit)) if (containsType(target->project()->projectIssues(kit), Task::TaskType::Error))
return false; return false;
if (!m_supportedProjectTypes.isEmpty()) if (!m_supportedProjectTypes.isEmpty())

View File

@@ -40,6 +40,7 @@
#include "session.h" #include "session.h"
#include "target.h" #include "target.h"
#include "targetsetuppage.h" #include "targetsetuppage.h"
#include "task.h"
#include <app/app_version.h> #include <app/app_version.h>
@@ -282,9 +283,12 @@ class TargetItem : public TypedTreeItem<TreeItem, TargetGroupItem>
public: public:
enum { DefaultPage = 0 }; // Build page. enum { DefaultPage = 0 }; // Build page.
TargetItem(Project *project, Id kitId) TargetItem(Project *project, Id kitId, const QList<Task> &issues)
: m_project(project), m_kitId(kitId) : m_project(project), m_kitId(kitId), m_kitIssues(issues)
{ {
m_kitWarningForProject = containsType(m_kitIssues, Task::TaskType::Warning);
m_kitErrorsForProject = containsType(m_kitIssues, Task::TaskType::Error);
updateSubItems(); updateSubItems();
} }
@@ -298,7 +302,8 @@ public:
Qt::ItemFlags flags(int column) const override Qt::ItemFlags flags(int column) const override
{ {
Q_UNUSED(column) Q_UNUSED(column)
return Qt::ItemFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); return m_kitErrorsForProject ? Qt::ItemFlags(0)
: Qt::ItemFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
} }
QVariant data(int column, int role) const override QVariant data(int column, int role) const override
@@ -313,11 +318,11 @@ public:
case Qt::DecorationRole: { case Qt::DecorationRole: {
const Kit *k = KitManager::kit(m_kitId); const Kit *k = KitManager::kit(m_kitId);
QTC_ASSERT(k, return QVariant()); QTC_ASSERT(k, return QVariant());
if (m_kitErrorsForProject)
return kitIconWithOverlay(*k, IconOverlay::Error);
if (!isEnabled()) if (!isEnabled())
return kitIconWithOverlay(*k, IconOverlay::Add); return kitIconWithOverlay(*k, IconOverlay::Add);
if (!k->isValid()) if (m_kitWarningForProject)
return kitIconWithOverlay(*k, IconOverlay::Error);
if (k->hasWarning())
return kitIconWithOverlay(*k, IconOverlay::Warning); return kitIconWithOverlay(*k, IconOverlay::Warning);
return k->icon(); return k->icon();
} }
@@ -341,9 +346,12 @@ public:
Kit *k = KitManager::kit(m_kitId); Kit *k = KitManager::kit(m_kitId);
QTC_ASSERT(k, return QVariant()); QTC_ASSERT(k, return QVariant());
QString toolTip; QString toolTip;
if (!isEnabled()) if (m_kitErrorsForProject)
toolTip = "<h3>" + tr("Kit is unsuited for Project") + "</h3>";
else if (!isEnabled())
toolTip = "<h3>" + tr("Click to activate:") + "</h3>"; toolTip = "<h3>" + tr("Click to activate:") + "</h3>";
toolTip += k->toHtml(); if (!m_kitIssues.isEmpty())
toolTip += toHtml(m_kitIssues);
return toolTip; return toolTip;
} }
@@ -466,12 +474,15 @@ public:
} }
} }
bool isEnabled() const { return target() != 0; } bool isEnabled() const { return target() != nullptr; }
public: public:
QPointer<Project> m_project; // Not owned. QPointer<Project> m_project; // Not owned.
Id m_kitId; Id m_kitId;
int m_currentChild = DefaultPage; // Use run page by default. int m_currentChild = DefaultPage;
bool m_kitErrorsForProject = false;
bool m_kitWarningForProject = false;
QList<Task> m_kitIssues;
private: private:
enum class IconOverlay { enum class IconOverlay {
@@ -526,10 +537,9 @@ public:
BuildOrRunItem(Project *project, Id kitId, SubIndex subIndex) BuildOrRunItem(Project *project, Id kitId, SubIndex subIndex)
: m_project(project), m_kitId(kitId), m_subIndex(subIndex) : m_project(project), m_kitId(kitId), m_subIndex(subIndex)
{ { }
}
~BuildOrRunItem() ~BuildOrRunItem() override
{ {
delete m_panel; delete m_panel;
} }
@@ -769,7 +779,7 @@ TargetItem *TargetGroupItem::targetItem(Target *target) const
Id needle = target->id(); // Unconfigured project have no active target. Id needle = target->id(); // Unconfigured project have no active target.
return findFirstLevelChild([needle](TargetItem *item) { return item->m_kitId == needle; }); return findFirstLevelChild([needle](TargetItem *item) { return item->m_kitId == needle; });
} }
return 0; return nullptr;
} }
void TargetGroupItemPrivate::handleRemovedKit(Kit *kit) void TargetGroupItemPrivate::handleRemovedKit(Kit *kit)
@@ -786,8 +796,7 @@ void TargetGroupItemPrivate::handleUpdatedKit(Kit *kit)
void TargetGroupItemPrivate::handleAddedKit(Kit *kit) void TargetGroupItemPrivate::handleAddedKit(Kit *kit)
{ {
if (m_project->supportsKit(kit)) q->appendChild(new TargetItem(m_project, kit->id(), m_project->projectIssues(kit)));
q->appendChild(new TargetItem(m_project, kit->id()));
} }
void TargetItem::updateSubItems() void TargetItem::updateSubItems()
@@ -805,11 +814,9 @@ void TargetGroupItemPrivate::rebuildContents()
{ {
q->removeChildren(); q->removeChildren();
const QList<Kit *> kits = KitManager::sortKits(KitManager::kits([this](const Kit *kit) { const QList<Kit *> kits = KitManager::sortKits(KitManager::kits());
return m_project->supportsKit(const_cast<Kit *>(kit));
}));
for (Kit *kit : kits) for (Kit *kit : kits)
q->appendChild(new TargetItem(m_project, kit->id())); q->appendChild(new TargetItem(m_project, kit->id(), m_project->projectIssues(kit)));
if (q->parent()) if (q->parent())
q->parent()->setData(0, QVariant::fromValue(static_cast<TreeItem *>(q)), q->parent()->setData(0, QVariant::fromValue(static_cast<TreeItem *>(q)),

View File

@@ -25,13 +25,15 @@
#include "task.h" #include "task.h"
#include "projectexplorerconstants.h"
#include <app/app_version.h> #include <app/app_version.h>
#include <texteditor/textmark.h> #include <texteditor/textmark.h>
#include <utils/algorithm.h>
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
#include "projectexplorerconstants.h"
#include <QTextStream> #include <QTextStream>
namespace ProjectExplorer namespace ProjectExplorer
@@ -174,4 +176,9 @@ QString toHtml(const QList<Task> &issues)
return result; return result;
} }
bool containsType(const QList<Task> &issues, Task::TaskType type)
{
return Utils::contains(issues, [type](const Task &t) { return t.type == type; });
}
} // namespace ProjectExplorer } // namespace ProjectExplorer

View File

@@ -99,6 +99,7 @@ uint PROJECTEXPLORER_EXPORT qHash(const Task &task);
bool PROJECTEXPLORER_EXPORT operator<(const Task &a, const Task &b); bool PROJECTEXPLORER_EXPORT operator<(const Task &a, const Task &b);
QString PROJECTEXPLORER_EXPORT toHtml(const QList<Task> &issues); QString PROJECTEXPLORER_EXPORT toHtml(const QList<Task> &issues);
bool PROJECTEXPLORER_EXPORT containsType(const QList<Task> &issues, Task::TaskType);
} //namespace ProjectExplorer } //namespace ProjectExplorer

View File

@@ -50,6 +50,7 @@
#include <projectexplorer/deploymentdata.h> #include <projectexplorer/deploymentdata.h>
#include <projectexplorer/headerpath.h> #include <projectexplorer/headerpath.h>
#include <projectexplorer/projectexplorer.h> #include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectexplorerconstants.h>
#include <projectexplorer/runconfiguration.h> #include <projectexplorer/runconfiguration.h>
#include <projectexplorer/target.h> #include <projectexplorer/target.h>
#include <projectexplorer/taskhub.h> #include <projectexplorer/taskhub.h>
@@ -596,12 +597,14 @@ void QmakeProject::buildFinished(bool success)
m_invalidateQmakeVfsContents = true; m_invalidateQmakeVfsContents = true;
} }
bool QmakeProject::supportsKit(const Kit *k, QString *errorMessage) const QList<Task> QmakeProject::projectIssues(const Kit *k) const
{ {
QtSupport::BaseQtVersion *version = QtSupport::QtKitInformation::qtVersion(k); QList<Task> result = Project::projectIssues(k);
if (!version && errorMessage) if (!QtSupport::QtKitInformation::qtVersion(k))
*errorMessage = tr("No Qt version set in kit."); result.append(createProjectTask(Task::TaskType::Error, tr("No Qt version set in kit.")));
return version; if (!ToolChainKitInformation::toolChain(k, ProjectExplorer::Constants::CXX_LANGUAGE_ID))
result.append(createProjectTask(Task::TaskType::Error, tr("No C++ compiler set in kit.")));
return result;
} }
// Find the folder that contains a file with a certain name (recurse down) // Find the folder that contains a file with a certain name (recurse down)

View File

@@ -60,7 +60,7 @@ public:
QmakeProFile *rootProFile() const; QmakeProFile *rootProFile() const;
bool supportsKit(const ProjectExplorer::Kit *k, QString *errorMesage) const final; QList<ProjectExplorer::Task> projectIssues(const ProjectExplorer::Kit *k) const final;
QmakeProFileNode *rootProjectNode() const final; QmakeProFileNode *rootProjectNode() const final;

View File

@@ -275,52 +275,42 @@ void QmlProject::refreshTargetDirectory()
updateDeploymentData(target); updateDeploymentData(target);
} }
bool QmlProject::supportsKit(const Kit *k, QString *errorMessage) const QList<Task> QmlProject::projectIssues(const Kit *k) const
{ {
if (!k->isValid()) { QList<Task> result = Project::projectIssues(k);
if (errorMessage)
*errorMessage = tr("Kit is not valid."); const QtSupport::BaseQtVersion *version = QtSupport::QtKitInformation::qtVersion(k);
return false; if (!version)
} result.append(createProjectTask(Task::TaskType::Error, tr("No Qt version set in kit.")));
IDevice::ConstPtr dev = DeviceKitInformation::device(k); IDevice::ConstPtr dev = DeviceKitInformation::device(k);
if (dev.isNull()) { if (dev.isNull())
if (errorMessage) result.append(createProjectTask(Task::TaskType::Error, tr("Kit has no device.")));
*errorMessage = tr("Kit has no device.");
return false;
}
QtSupport::BaseQtVersion *version = QtSupport::QtKitInformation::qtVersion(k); if (version && version->qtVersion() < QtSupport::QtVersionNumber(5, 0, 0))
if (!version) { result.append(createProjectTask(Task::TaskType::Error, tr("Qt version is too old.")));
if (errorMessage)
*errorMessage = tr("No Qt version set in kit."); if (dev.isNull() || !version)
return false; return result; // No need to check deeper than this
}
if (version->qtVersion() < QtSupport::QtVersionNumber(5, 0, 0)) {
if (errorMessage)
*errorMessage = tr("Qt version is too old.");
return false;
}
if (dev->type() == ProjectExplorer::Constants::DESKTOP_DEVICE_TYPE) { if (dev->type() == ProjectExplorer::Constants::DESKTOP_DEVICE_TYPE) {
if (version->type() == QtSupport::Constants::DESKTOPQT) { if (version->type() == QtSupport::Constants::DESKTOPQT) {
if (static_cast<QtSupport::DesktopQtVersion *>(version)->qmlsceneCommand().isEmpty()) { if (static_cast<const QtSupport::DesktopQtVersion *>(version)->qmlsceneCommand().isEmpty()) {
if (errorMessage) result.append(createProjectTask(Task::TaskType::Error,
*errorMessage = tr("Qt version has no qmlscene command."); tr("Qt version has no qmlscene command.")));
return false;
} }
} else { } else {
// Non-desktop Qt on a desktop device? We don't support that. // Non-desktop Qt on a desktop device? We don't support that.
if (errorMessage) result.append(createProjectTask(Task::TaskType::Error,
*errorMessage = tr("Non-desktop Qt is used with a Desktop device."); tr("Non-desktop Qt is used with a Desktop device.")));
return false;
} }
} } else {
// If not a desktop device, don't check the Qt version for qmlscene. // If not a desktop device, don't check the Qt version for qmlscene.
// The device is responsible for providing it and we assume qmlscene can be found // The device is responsible for providing it and we assume qmlscene can be found
// in $PATH if it's not explicitly given. // in $PATH if it's not explicitly given.
return true; }
return result;
} }
Project::RestoreResult QmlProject::fromMap(const QVariantMap &map, QString *errorMessage) Project::RestoreResult QmlProject::fromMap(const QVariantMap &map, QString *errorMessage)
@@ -334,7 +324,9 @@ Project::RestoreResult QmlProject::fromMap(const QVariantMap &map, QString *erro
if (!activeTarget()) { if (!activeTarget()) {
// find a kit that matches prerequisites (prefer default one) // find a kit that matches prerequisites (prefer default one)
const QList<Kit*> kits = KitManager::kits([this](const Kit *k) { return supportsKit(k, nullptr); }); const QList<Kit*> kits = KitManager::kits([this](const Kit *k) {
return !containsType(projectIssues(k), Task::TaskType::Error);
});
if (!kits.isEmpty()) { if (!kits.isEmpty()) {
Kit *kit = kits.contains(KitManager::defaultKit()) ? KitManager::defaultKit() : kits.first(); Kit *kit = kits.contains(KitManager::defaultKit()) ? KitManager::defaultKit() : kits.first();

View File

@@ -48,7 +48,7 @@ public:
explicit QmlProject(const Utils::FileName &filename); explicit QmlProject(const Utils::FileName &filename);
~QmlProject() override; ~QmlProject() override;
bool supportsKit(const ProjectExplorer::Kit *k, QString *errorMessage = nullptr) const final; QList<ProjectExplorer::Task> projectIssues(const ProjectExplorer::Kit *k) const final;
bool validProjectFile() const; bool validProjectFile() const;