cmake: Add support for custom startup programs for executable targets

CMake supports the use of custom startup programs that are provided
in the IDE to simplify execution.

If the build system provides launchers, these are provided as an
additional selection field of the run configuration including an
entry without launcher.

As of cmake version 3.29, the start programs are extracted from
the API of the cmake file. For older cmake versions, a launcher
is initialized from the cmake variable CMAKE_CROSSCOMPILING_EMULATOR,
if available.

Fixes: QTCREATORBUG-29880
Change-Id: I4345b56c9ca5befb5876a361e7da4675590399ca
Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
Reviewed-by: Cristian Adam <cristian.adam@qt.io>
This commit is contained in:
Ralf Habacker
2024-03-15 16:25:06 +01:00
parent 6b1e7eff93
commit 94663d0db7
11 changed files with 294 additions and 1 deletions

View File

@@ -1977,6 +1977,9 @@ static FilePaths librarySearchPaths(const CMakeBuildSystem *bs, const QString &b
const QList<BuildTargetInfo> CMakeBuildSystem::appTargets() const
{
const CMakeConfig &cm = configurationFromCMake();
QString emulator = cm.stringValueOf("CMAKE_CROSSCOMPILING_EMULATOR");
QList<BuildTargetInfo> appTargetList;
const bool forAndroid = DeviceTypeKitAspect::deviceTypeId(kit())
== Android::Constants::ANDROID_DEVICE_TYPE;
@@ -1989,6 +1992,15 @@ const QList<BuildTargetInfo> CMakeBuildSystem::appTargets() const
BuildTargetInfo bti;
bti.displayName = ct.title;
if (ct.launchers.size() > 0)
bti.launchers = ct.launchers;
else if (!emulator.isEmpty()) {
// fallback for cmake < 3.29
QStringList args = emulator.split(";");
FilePath command = FilePath::fromString(args.takeFirst());
LauncherInfo launcherInfo = { "emulator", command, args };
bti.launchers.append(Launcher(launcherInfo, ct.sourceDirectory));
}
bti.targetFilePath = ct.executable;
bti.projectFilePath = ct.sourceDirectory.cleanPath();
bti.workingDirectory = ct.workingDirectory;

View File

@@ -7,6 +7,7 @@
#include <projectexplorer/projectmacro.h>
#include <projectexplorer/projectnodes.h>
#include <projectexplorer/runconfigurationaspects.h>
#include <utils/fileutils.h>
@@ -30,6 +31,7 @@ class CMAKE_EXPORT CMakeBuildTarget
public:
QString title;
Utils::FilePath executable; // TODO: rename to output?
QList<ProjectExplorer::Launcher> launchers;
TargetType targetType = UtilityType;
bool linksToQtGui = false;
bool qtcRunnable = true;

View File

@@ -342,6 +342,22 @@ static CMakeBuildTarget toBuildTarget(const TargetDetails &t,
}
ct.libraryDirectories = filteredUnique(librarySeachPaths);
qCInfo(cmakeLogger) << "libraryDirectories for target" << ct.title << ":" << ct.libraryDirectories;
// If there are start programs, there should also be an option to select none
if (!t.launcherInfos.isEmpty()) {
LauncherInfo info { "unused", Utils::FilePath(), QStringList() };
ct.launchers.append(Launcher(info, sourceDirectory));
}
// if there is a test and an emulator launcher, add the emulator and
// also a combination as the last entry, but not the "test" launcher
// as it will not work for cross-compiled executables
if (t.launcherInfos.size() == 2 && t.launcherInfos[0].type == "test" && t.launcherInfos[1].type == "emulator") {
ct.launchers.append(Launcher(t.launcherInfos[1], sourceDirectory));
ct.launchers.append(Launcher(t.launcherInfos[0], t.launcherInfos[1], sourceDirectory));
} else if (t.launcherInfos.size() == 1) {
Launcher launcher(t.launcherInfos[0], sourceDirectory);
ct.launchers.append(launcher);
}
}
return ct;
}

View File

@@ -11,6 +11,7 @@
#include <projectexplorer/rawprojectpart.h>
#include <utils/algorithm.h>
#include <utils/filepath.h>
#include <utils/qtcassert.h>
#include <QGuiApplication>
@@ -650,6 +651,19 @@ static TargetDetails extractTargetDetails(const QJsonObject &root, QString &erro
};
});
}
{
const QJsonArray launchers = root.value("launchers").toArray();
if (launchers.size() > 0) {
t.launcherInfos = transform<QList>(launchers, [](const QJsonValue &v) {
const QJsonObject o = v.toObject();
QList<QString> arguments;
for (const QJsonValue &arg : o.value("arguments").toArray())
arguments.append(arg.toString());
FilePath command = FilePath::fromString(o.value("command").toString());
return ProjectExplorer::LauncherInfo { o.value("type").toString(), command, arguments };
});
}
}
return t;
}

View File

@@ -197,6 +197,7 @@ public:
QList<Utils::FilePath> artifacts;
QString installPrefix;
std::vector<InstallDestination> installDestination;
QList<ProjectExplorer::LauncherInfo> launcherInfos;
std::optional<LinkInfo> link;
std::optional<ArchiveInfo> archive;
std::vector<DependencyInfo> dependencies;

View File

@@ -5,12 +5,15 @@
#include "projectexplorer_export.h"
#include "runconfiguration.h"
#include <utils/environment.h>
#include <utils/filepath.h>
#include <QList>
namespace ProjectExplorer {
class Launcher;
class PROJECTEXPLORER_EXPORT BuildTargetInfo
{
@@ -19,6 +22,7 @@ public:
QString displayName;
QString displayNameUniquifier;
QList<Launcher> launchers;
Utils::FilePath targetFilePath;
Utils::FilePath projectFilePath;
Utils::FilePath workingDirectory;

View File

@@ -70,6 +70,7 @@ private:
FilePath executableToRun(const BuildTargetInfo &targetInfo) const;
const Kind m_kind;
LauncherAspect launcher{this};
EnvironmentAspect environment{this};
ExecutableAspect executable{this};
ArgumentsAspect arguments{this};
@@ -90,6 +91,8 @@ void DesktopRunConfiguration::updateTargetInformation()
auto terminalAspect = aspect<TerminalAspect>();
terminalAspect->setUseTerminalHint(bti.targetFilePath.needsDevice() ? false : bti.usesTerminal);
terminalAspect->setEnabled(!bti.targetFilePath.needsDevice());
auto launcherAspect = aspect<LauncherAspect>();
launcherAspect->setVisible(false);
if (m_kind == Qmake) {
@@ -121,6 +124,12 @@ void DesktopRunConfiguration::updateTargetInformation()
} else if (m_kind == CMake) {
if (bti.launchers.size() > 0) {
launcherAspect->setVisible(true);
// Use start program by default, if defined (see toBuildTarget() for details)
launcherAspect->setDefaultLauncher(bti.launchers.last());
launcherAspect->updateLaunchers(bti.launchers);
}
aspect<ExecutableAspect>()->setExecutable(bti.targetFilePath);
aspect<WorkingDirectoryAspect>()->setDefaultWorkingDirectory(bti.workingDirectory);
emit aspect<EnvironmentAspect>()->environmentChanged();

View File

@@ -173,6 +173,9 @@ RunConfiguration::RunConfiguration(Target *target, Utils::Id id)
m_commandLineGetter = [this] {
Launcher launcher;
if (const auto launcherAspect = aspect<LauncherAspect>())
launcher = launcherAspect->currentLauncher();
FilePath executable;
if (const auto executableAspect = aspect<ExecutableAspect>())
executable = executableAspect->executable();
@@ -180,7 +183,14 @@ RunConfiguration::RunConfiguration(Target *target, Utils::Id id)
if (const auto argumentsAspect = aspect<ArgumentsAspect>())
arguments = argumentsAspect->arguments();
return CommandLine{executable, arguments, CommandLine::Raw};
if (launcher.command.isEmpty())
return CommandLine{executable, arguments, CommandLine::Raw};
CommandLine launcherCommand(launcher.command, launcher.arguments);
launcherCommand.addArg(executable.toString());
launcherCommand.addArgs(arguments, CommandLine::Raw);
return launcherCommand;
};
}

View File

@@ -28,6 +28,51 @@ class RunConfigurationFactory;
class RunConfiguration;
class RunConfigurationCreationInfo;
class Target;
class BuildTargetInfo;
/**
* Contains start program entries that are retrieved
* from the cmake file api
*/
class LauncherInfo
{
public:
QString type;
Utils::FilePath command;
QStringList arguments;
};
/**
* Contains a start program entry that is displayed in the run configuration interface.
*
* This follows the design for the use of "Test Launcher", the
* Wrappers for running executables on the host system and "Emulator",
* wrappers for cross-compiled applications, which are supported for
* example by the cmake build system.
*/
class PROJECTEXPLORER_EXPORT Launcher
{
public:
Launcher() = default;
/// Create a single launcher from the \p launcherInfo parameter, which can be of type "Test launcher" or "Emulator"
Launcher(const LauncherInfo &launcherInfo, const Utils::FilePath &sourceDirectory);
/// Create a combined launcher from the passed info parameters, with \p testLauncherInfo
/// as first and \p emulatorLauncherInfo appended
Launcher(const LauncherInfo &testLauncherInfo, const LauncherInfo &emulatorlauncherInfo, const Utils::FilePath &sourceDirectory);
bool operator==(const Launcher &other) const
{
return id == other.id && displayName == other.displayName && command == other.command
&& arguments == other.arguments;
}
QString id;
QString displayName;
Utils::FilePath command;
QStringList arguments;
};
/**
* An interface to facilitate switching between hunks of

View File

@@ -797,6 +797,156 @@ Interpreter::Interpreter(const QString &_id,
, autoDetected(_autoDetected)
{}
static QString launcherType2UiString(const QString &type)
{
if (type == "test")
return Tr::tr("Test");
else if (type == "emulator")
return Tr::tr("Emulator");
return QString();
}
Launcher::Launcher(const LauncherInfo &launcherInfo, const FilePath &sourceDirectory)
: id(launcherInfo.type)
, arguments(launcherInfo.arguments)
{
if (launcherInfo.type != "unused") {
command = launcherInfo.command;
if (command.isRelativePath())
command = sourceDirectory.resolvePath(command);
displayName = QString("%1 (%2)").arg(launcherType2UiString(launcherInfo.type),
CommandLine(command, arguments).displayName());
}
}
Launcher::Launcher(const LauncherInfo &testLauncherInfo, const LauncherInfo &emulatorLauncherInfo, const Utils::FilePath &sourceDirectory)
: id(testLauncherInfo.type + " + " + emulatorLauncherInfo.type)
, command(testLauncherInfo.command)
, arguments(testLauncherInfo.arguments)
{
if (command.isRelativePath())
command = sourceDirectory.resolvePath(command);
FilePath command1 = emulatorLauncherInfo.command;
if (command1.isRelativePath())
command1 = sourceDirectory.resolvePath(command1);
arguments.append(command1.toString());
arguments.append(emulatorLauncherInfo.arguments);
displayName = QString("%1 + %2 (%3)").arg(launcherType2UiString(testLauncherInfo.type),
launcherType2UiString(emulatorLauncherInfo.type),
CommandLine(command, arguments).displayName());
}
/*!
\class ProjectExplorer::LauncherAspect
\inmodule QtCreator
\brief With the LauncherAspect class, a user can specify a launcher program for
use with executable files for which a launcher program is optionally available.
*/
LauncherAspect::LauncherAspect(AspectContainer *container)
: BaseAspect(container)
{
addDataExtractor(this, &LauncherAspect::currentLauncher, &Data::launcher);
}
Launcher LauncherAspect::currentLauncher() const
{
return Utils::findOrDefault(m_launchers, Utils::equal(&Launcher::id, m_currentId));
}
void LauncherAspect::updateLaunchers(const QList<Launcher> &launchers)
{
if (m_launchers == launchers)
return;
m_launchers = launchers;
if (m_comboBox)
updateComboBox();
}
void LauncherAspect::setDefaultLauncher(const Launcher &launcher)
{
if (m_defaultId == launcher.id)
return;
m_defaultId = launcher.id;
if (m_currentId.isEmpty())
setCurrentLauncher(launcher);
}
void LauncherAspect::setCurrentLauncher(const Launcher &launcher)
{
if (m_comboBox) {
const int index = m_launchers.indexOf(launcher);
if (index < 0 || index >= m_comboBox->count())
return;
m_comboBox->setCurrentIndex(index);
} else {
setCurrentLauncherId(launcher.id);
}
}
void LauncherAspect::fromMap(const Store &map)
{
setCurrentLauncherId(map.value(settingsKey(), m_defaultId).toString());
}
void LauncherAspect::toMap(Store &map) const
{
if (m_currentId != m_defaultId)
saveToMap(map, m_currentId, QString(), settingsKey());
}
void LauncherAspect::addToLayout(Layout &builder)
{
if (QTC_GUARD(m_comboBox.isNull()))
m_comboBox = new QComboBox;
updateComboBox();
connect(m_comboBox, &QComboBox::currentIndexChanged,
this, &LauncherAspect::updateCurrentLauncher);
builder.addItems({Tr::tr("Launcher:"), m_comboBox.data()});
}
void LauncherAspect::setCurrentLauncherId(const QString &id)
{
if (id == m_currentId)
return;
m_currentId = id;
emit changed();
}
void LauncherAspect::updateCurrentLauncher()
{
const int index = m_comboBox->currentIndex();
if (index < 0)
return;
QTC_ASSERT(index < m_launchers.size(), return);
m_comboBox->setToolTip(m_launchers[index].command.toUserOutput());
setCurrentLauncherId(m_launchers[index].id);
}
void LauncherAspect::updateComboBox()
{
int currentIndex = -1;
int defaultIndex = -1;
m_comboBox->clear();
for (const Launcher &launcher : std::as_const(m_launchers)) {
int index = m_comboBox->count();
m_comboBox->addItem(launcher.displayName);
m_comboBox->setItemData(index, launcher.command.toUserOutput(), Qt::ToolTipRole);
if (launcher.id == m_currentId)
currentIndex = index;
if (launcher.id == m_defaultId)
defaultIndex = index;
}
if (currentIndex >= 0)
m_comboBox->setCurrentIndex(currentIndex);
else if (defaultIndex >= 0)
m_comboBox->setCurrentIndex(defaultIndex);
updateCurrentLauncher();
}
/*!
\class ProjectExplorer::X11ForwardingAspect
\inmodule QtCreator

View File

@@ -227,6 +227,36 @@ public:
QString detectionSource;
};
class PROJECTEXPLORER_EXPORT LauncherAspect : public Utils::BaseAspect
{
Q_OBJECT
public:
LauncherAspect(Utils::AspectContainer *container = nullptr);
Launcher currentLauncher() const;
void updateLaunchers(const QList<Launcher> &launchers);
void setDefaultLauncher(const Launcher &launcher);
void setCurrentLauncher(const Launcher &launcher);
void setSettingsDialogId(Utils::Id id) { m_settingsDialogId = id; }
void fromMap(const Utils::Store &) override;
void toMap(Utils::Store &) const override;
void addToLayout(Layouting::Layout &parent) override;
struct Data : Utils::BaseAspect::Data { Launcher launcher; };
private:
void setCurrentLauncherId(const QString &id);
void updateCurrentLauncher();
void updateComboBox();
QList<Launcher> m_launchers;
QPointer<QComboBox> m_comboBox;
QString m_defaultId;
QString m_currentId;
Utils::Id m_settingsDialogId;
};
class PROJECTEXPLORER_EXPORT MainScriptAspect : public Utils::FilePathAspect
{
Q_OBJECT