From 94663d0db7b24b638cc3c9bbd65a1f922b28e2c3 Mon Sep 17 00:00:00 2001 From: Ralf Habacker Date: Fri, 15 Mar 2024 16:25:06 +0100 Subject: [PATCH] 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 Reviewed-by: Cristian Adam --- .../cmakeprojectmanager/cmakebuildsystem.cpp | 12 ++ .../cmakeprojectmanager/cmakebuildtarget.h | 2 + .../fileapidataextractor.cpp | 16 ++ .../cmakeprojectmanager/fileapiparser.cpp | 14 ++ .../cmakeprojectmanager/fileapiparser.h | 1 + src/plugins/projectexplorer/buildtargetinfo.h | 4 + .../desktoprunconfiguration.cpp | 9 ++ .../projectexplorer/runconfiguration.cpp | 12 +- .../projectexplorer/runconfiguration.h | 45 ++++++ .../runconfigurationaspects.cpp | 150 ++++++++++++++++++ .../projectexplorer/runconfigurationaspects.h | 30 ++++ 11 files changed, 294 insertions(+), 1 deletion(-) diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp index 13aee2f2de9..ec5e3a4b89f 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp @@ -1977,6 +1977,9 @@ static FilePaths librarySearchPaths(const CMakeBuildSystem *bs, const QString &b const QList CMakeBuildSystem::appTargets() const { + const CMakeConfig &cm = configurationFromCMake(); + QString emulator = cm.stringValueOf("CMAKE_CROSSCOMPILING_EMULATOR"); + QList appTargetList; const bool forAndroid = DeviceTypeKitAspect::deviceTypeId(kit()) == Android::Constants::ANDROID_DEVICE_TYPE; @@ -1989,6 +1992,15 @@ const QList 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; diff --git a/src/plugins/cmakeprojectmanager/cmakebuildtarget.h b/src/plugins/cmakeprojectmanager/cmakebuildtarget.h index d40729a3970..934070bdfd1 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildtarget.h +++ b/src/plugins/cmakeprojectmanager/cmakebuildtarget.h @@ -7,6 +7,7 @@ #include #include +#include #include @@ -30,6 +31,7 @@ class CMAKE_EXPORT CMakeBuildTarget public: QString title; Utils::FilePath executable; // TODO: rename to output? + QList launchers; TargetType targetType = UtilityType; bool linksToQtGui = false; bool qtcRunnable = true; diff --git a/src/plugins/cmakeprojectmanager/fileapidataextractor.cpp b/src/plugins/cmakeprojectmanager/fileapidataextractor.cpp index 6f4050a3aa4..7a6de1749de 100644 --- a/src/plugins/cmakeprojectmanager/fileapidataextractor.cpp +++ b/src/plugins/cmakeprojectmanager/fileapidataextractor.cpp @@ -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; } diff --git a/src/plugins/cmakeprojectmanager/fileapiparser.cpp b/src/plugins/cmakeprojectmanager/fileapiparser.cpp index d404d124db8..d9ea457600f 100644 --- a/src/plugins/cmakeprojectmanager/fileapiparser.cpp +++ b/src/plugins/cmakeprojectmanager/fileapiparser.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -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(launchers, [](const QJsonValue &v) { + const QJsonObject o = v.toObject(); + QList 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; } diff --git a/src/plugins/cmakeprojectmanager/fileapiparser.h b/src/plugins/cmakeprojectmanager/fileapiparser.h index 9043d38c260..f6a59edae2a 100644 --- a/src/plugins/cmakeprojectmanager/fileapiparser.h +++ b/src/plugins/cmakeprojectmanager/fileapiparser.h @@ -197,6 +197,7 @@ public: QList artifacts; QString installPrefix; std::vector installDestination; + QList launcherInfos; std::optional link; std::optional archive; std::vector dependencies; diff --git a/src/plugins/projectexplorer/buildtargetinfo.h b/src/plugins/projectexplorer/buildtargetinfo.h index 4ba1c4de5b4..c99dec64308 100644 --- a/src/plugins/projectexplorer/buildtargetinfo.h +++ b/src/plugins/projectexplorer/buildtargetinfo.h @@ -5,12 +5,15 @@ #include "projectexplorer_export.h" +#include "runconfiguration.h" + #include #include #include namespace ProjectExplorer { +class Launcher; class PROJECTEXPLORER_EXPORT BuildTargetInfo { @@ -19,6 +22,7 @@ public: QString displayName; QString displayNameUniquifier; + QList launchers; Utils::FilePath targetFilePath; Utils::FilePath projectFilePath; Utils::FilePath workingDirectory; diff --git a/src/plugins/projectexplorer/desktoprunconfiguration.cpp b/src/plugins/projectexplorer/desktoprunconfiguration.cpp index a8d3284e9be..02f31c13452 100644 --- a/src/plugins/projectexplorer/desktoprunconfiguration.cpp +++ b/src/plugins/projectexplorer/desktoprunconfiguration.cpp @@ -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->setUseTerminalHint(bti.targetFilePath.needsDevice() ? false : bti.usesTerminal); terminalAspect->setEnabled(!bti.targetFilePath.needsDevice()); + auto launcherAspect = aspect(); + 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()->setExecutable(bti.targetFilePath); aspect()->setDefaultWorkingDirectory(bti.workingDirectory); emit aspect()->environmentChanged(); diff --git a/src/plugins/projectexplorer/runconfiguration.cpp b/src/plugins/projectexplorer/runconfiguration.cpp index 2f5dfc2513d..3c78b37cc44 100644 --- a/src/plugins/projectexplorer/runconfiguration.cpp +++ b/src/plugins/projectexplorer/runconfiguration.cpp @@ -173,6 +173,9 @@ RunConfiguration::RunConfiguration(Target *target, Utils::Id id) m_commandLineGetter = [this] { + Launcher launcher; + if (const auto launcherAspect = aspect()) + launcher = launcherAspect->currentLauncher(); FilePath executable; if (const auto executableAspect = aspect()) executable = executableAspect->executable(); @@ -180,7 +183,14 @@ RunConfiguration::RunConfiguration(Target *target, Utils::Id id) if (const auto argumentsAspect = aspect()) 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; }; } diff --git a/src/plugins/projectexplorer/runconfiguration.h b/src/plugins/projectexplorer/runconfiguration.h index 2054a840c14..b49ad3e7d77 100644 --- a/src/plugins/projectexplorer/runconfiguration.h +++ b/src/plugins/projectexplorer/runconfiguration.h @@ -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 diff --git a/src/plugins/projectexplorer/runconfigurationaspects.cpp b/src/plugins/projectexplorer/runconfigurationaspects.cpp index 01003a97668..6f49b61c0e8 100644 --- a/src/plugins/projectexplorer/runconfigurationaspects.cpp +++ b/src/plugins/projectexplorer/runconfigurationaspects.cpp @@ -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 &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 diff --git a/src/plugins/projectexplorer/runconfigurationaspects.h b/src/plugins/projectexplorer/runconfigurationaspects.h index d891132ea7d..69b55c9974b 100644 --- a/src/plugins/projectexplorer/runconfigurationaspects.h +++ b/src/plugins/projectexplorer/runconfigurationaspects.h @@ -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 &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 m_launchers; + QPointer m_comboBox; + QString m_defaultId; + QString m_currentId; + Utils::Id m_settingsDialogId; +}; + class PROJECTEXPLORER_EXPORT MainScriptAspect : public Utils::FilePathAspect { Q_OBJECT