From 090b540487c0d5e178a043f8df05d3bbc0b67bd5 Mon Sep 17 00:00:00 2001 From: Markus Redeker Date: Mon, 30 Sep 2024 16:41:30 +0200 Subject: [PATCH] Extend Coco plugin for QMake/CMake project instrumentation The Coco plugin now can also configure QMake and CMake projects for the use with Coco. (COCO-1782) * There is a new global preferences page to set the Coco directory. (But the plugin also searches automatically for the active Coco installation, so using the page should rarely be necessary.) * There is a project settings page where the code coverage can be configured, especially CoverageScanner options set. * Code coverage is enabled by changing the build settings of a project so that the build tool reads a special file at the beginning (cocoplugin.prf or cocoplugin.cmake), which replaces the normal compiler calls with calls of the Coco compiler wrappers. The CoverageScanner options are part of this file. * An additional, ficticious build step appears in the build menu of QMake and CMake projects, with a button to switch coverage on or off. * Added documentation * The class CMakeBuildSystem has been made publicly accessible and some member functions added so that the build settings chanbe chenged. [ChangeLog][coco] Added to the existing Coco plugin the ability to configure CMake and QMake projects for code coverage. Change-Id: I70a351b79b89bef3c25f81bb4b62ac59dc787645 Reviewed-by: Leena Miettinen Reviewed-by: hjk --- .../images/qtcreator-coco-buildstep.png | Bin 0 -> 6488 bytes .../images/qtcreator-coco-configpage.png | Bin 0 -> 16561 bytes doc/qtcreator/images/qtcreator-coco.png | Bin 5550 -> 7353 bytes doc/qtcreator/src/analyze/creator-coco.qdoc | 113 +++++- .../cmakeprojectmanager/builddirparameters.h | 6 +- .../cmakebuildconfiguration.cpp | 17 +- .../cmakebuildconfiguration.h | 12 +- .../cmakeprojectmanager/cmakebuildsystem.cpp | 5 +- .../cmakeprojectmanager/cmakebuildsystem.h | 19 +- src/plugins/coco/CMakeLists.txt | 48 ++- src/plugins/coco/Coco.json.in | 5 +- src/plugins/coco/coco.qbs | 45 ++- src/plugins/coco/cocobuild/buildsettings.cpp | 100 ++++++ src/plugins/coco/cocobuild/buildsettings.h | 67 ++++ .../coco/cocobuild/cmakemodificationfile.cpp | 91 +++++ .../coco/cocobuild/cmakemodificationfile.h | 32 ++ src/plugins/coco/cocobuild/cocobuildstep.cpp | 126 +++++++ src/plugins/coco/cocobuild/cocobuildstep.h | 59 +++ .../coco/cocobuild/cococmakesettings.cpp | 167 +++++++++ .../coco/cocobuild/cococmakesettings.h | 51 +++ .../coco/cocobuild/cocoprojectwidget.cpp | 336 ++++++++++++++++++ .../coco/cocobuild/cocoprojectwidget.h | 81 +++++ .../coco/cocobuild/cocoqmakesettings.cpp | 188 ++++++++++ .../coco/cocobuild/cocoqmakesettings.h | 56 +++ .../coco/cocobuild/modificationfile.cpp | 73 ++++ src/plugins/coco/cocobuild/modificationfile.h | 51 +++ .../coco/cocobuild/qmakefeaturefile.cpp | 105 ++++++ src/plugins/coco/cocobuild/qmakefeaturefile.h | 34 ++ src/plugins/coco/cocoplugin.cpp | 150 ++++++-- src/plugins/coco/cocoplugin.qrc | 10 + src/plugins/coco/cocoplugin_global.h | 12 + src/plugins/coco/cocopluginconstants.h | 23 ++ src/plugins/coco/common.cpp | 30 ++ src/plugins/coco/common.h | 14 + src/plugins/coco/files/cocoplugin-clang.cmake | 8 + src/plugins/coco/files/cocoplugin-gcc.cmake | 8 + .../coco/files/cocoplugin-visualstudio.cmake | 8 + src/plugins/coco/files/cocoplugin.cmake | 87 +++++ src/plugins/coco/files/cocoplugin.prf | 33 ++ src/plugins/coco/images/SquishCoco_48x48.png | Bin 0 -> 594 bytes .../coco/settings/cocoinstallation.cpp | 177 +++++++++ src/plugins/coco/settings/cocoinstallation.h | 39 ++ .../settings/cocoprojectsettingswidget.cpp | 40 +++ .../coco/settings/cocoprojectsettingswidget.h | 28 ++ src/plugins/coco/settings/globalsettings.cpp | 53 +++ src/plugins/coco/settings/globalsettings.h | 13 + .../coco/settings/globalsettingspage.cpp | 121 +++++++ .../coco/settings/globalsettingspage.h | 59 +++ 48 files changed, 2724 insertions(+), 76 deletions(-) create mode 100644 doc/qtcreator/images/qtcreator-coco-buildstep.png create mode 100644 doc/qtcreator/images/qtcreator-coco-configpage.png create mode 100644 src/plugins/coco/cocobuild/buildsettings.cpp create mode 100644 src/plugins/coco/cocobuild/buildsettings.h create mode 100644 src/plugins/coco/cocobuild/cmakemodificationfile.cpp create mode 100644 src/plugins/coco/cocobuild/cmakemodificationfile.h create mode 100644 src/plugins/coco/cocobuild/cocobuildstep.cpp create mode 100644 src/plugins/coco/cocobuild/cocobuildstep.h create mode 100644 src/plugins/coco/cocobuild/cococmakesettings.cpp create mode 100644 src/plugins/coco/cocobuild/cococmakesettings.h create mode 100644 src/plugins/coco/cocobuild/cocoprojectwidget.cpp create mode 100644 src/plugins/coco/cocobuild/cocoprojectwidget.h create mode 100644 src/plugins/coco/cocobuild/cocoqmakesettings.cpp create mode 100644 src/plugins/coco/cocobuild/cocoqmakesettings.h create mode 100644 src/plugins/coco/cocobuild/modificationfile.cpp create mode 100644 src/plugins/coco/cocobuild/modificationfile.h create mode 100644 src/plugins/coco/cocobuild/qmakefeaturefile.cpp create mode 100644 src/plugins/coco/cocobuild/qmakefeaturefile.h create mode 100644 src/plugins/coco/cocoplugin.qrc create mode 100644 src/plugins/coco/cocoplugin_global.h create mode 100644 src/plugins/coco/cocopluginconstants.h create mode 100644 src/plugins/coco/common.cpp create mode 100644 src/plugins/coco/common.h create mode 100644 src/plugins/coco/files/cocoplugin-clang.cmake create mode 100644 src/plugins/coco/files/cocoplugin-gcc.cmake create mode 100644 src/plugins/coco/files/cocoplugin-visualstudio.cmake create mode 100644 src/plugins/coco/files/cocoplugin.cmake create mode 100644 src/plugins/coco/files/cocoplugin.prf create mode 100644 src/plugins/coco/images/SquishCoco_48x48.png create mode 100644 src/plugins/coco/settings/cocoinstallation.cpp create mode 100644 src/plugins/coco/settings/cocoinstallation.h create mode 100644 src/plugins/coco/settings/cocoprojectsettingswidget.cpp create mode 100644 src/plugins/coco/settings/cocoprojectsettingswidget.h create mode 100644 src/plugins/coco/settings/globalsettings.cpp create mode 100644 src/plugins/coco/settings/globalsettings.h create mode 100644 src/plugins/coco/settings/globalsettingspage.cpp create mode 100644 src/plugins/coco/settings/globalsettingspage.h diff --git a/doc/qtcreator/images/qtcreator-coco-buildstep.png b/doc/qtcreator/images/qtcreator-coco-buildstep.png new file mode 100644 index 0000000000000000000000000000000000000000..66b21eee1e7eba0d8770e96301f5fd3cfa21bbd2 GIT binary patch literal 6488 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 AjsO4v literal 0 HcmV?d00001 diff --git a/doc/qtcreator/images/qtcreator-coco-configpage.png b/doc/qtcreator/images/qtcreator-coco-configpage.png new file mode 100644 index 0000000000000000000000000000000000000000..ac48dcbbd81c6aa9f7f85f2b2a61402c6ff1914e GIT binary patch literal 16561 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 F69Bvei#=yX^O=aVL1_lPk;vjb?hIQv;UNSH+ zu%tWsIx;Y9?C1WI$jZRLz**oCSB9KB4DzLN%!)t(@I#XG}=PBX?KD^iwi(jCU(_B@{h7<-KUxx;1-a?&z=9d*W>( zwfv^Mt{8%@031i9y(`x{&xQNlJ6zoznnRA zZrAVsfoH$`zL{_T_v8ELmDOq8vu4i@=Gpo26!+mTFE78mu`#)FySSRqj0xetudWWi z99w>OY4AnwPFv;jzB9=Hxd1<<;`-huWI&9$P85_{;KX z6*lvBeY=%iYR168z|hu}dwW};p^X#+1H%G?4mR=Er66Gu1`b0828I9!g(D0M3=WL} z3tu)iGR|dTU|={9&}5keQhh+=@3-yyePhe-mRd=knYFKU&z4sq74vp};M@xea)!y< zwr%?$SGZ4!2V`QsQ*Hl`>%WBr5*Zj67SB7SUo-Jv`scK{r(90$`*3vs^_6d0<|)^` z($CI(aG24m`i;o0*T&l~CX23q*?B@goL*`C0KDVse?ssk0stO0)O?s7y z9x6g#I{j-7MHT&=={5P(DX-OkC#iau=rJ%b6uhcBFQHwZ(^7K(|V56 zT|=?%`+?nCvcKm=S^VVsb>_$Z#nt!YeE;97HGgZnuRpA2)2wBEw%zMyma*1W-BEve ze7D>p(YfUp{L5{9*!|@$+5Ra0|Mz=vaq-;~&a+W+RWGnQO$UC4PD>|2j{1ru!^8?`PK^_j|wn z#q9jOe(e1+i>80iUuJyVXHoHS-{mtOrw5$hc=yY-i`zs0Cad>f{MgukWtY6tj_-Gh z`L}N0{#vHF^!2qse}8`sL(z#fe?A`9lhIQDs^80L$tldLbK{(AS!N__S2)8?%3@)3%JZ++uz&9y0sP zvboPTe$T_H+Tp8mJ?8yPP)Ig2Dd%enN$DoXVux++wa%S*4z6fNc+j( zxB2zAxe~qSFH1NzKfvVk`@P@eR97f{c$8cJI{JRl&qt}=wUV17!}%I)d-UY?J@Kvw zC0_x{$|LT6a*N}>wJqNqo>O$q;&5@z_XBdb$_wP@{`kN$_s@$Zr=)dum%Y7|eX!#K zo1#;0)tN_)-nEx!i|YSgIz28ZRq0aNzK)ghLI3-#-(ApLWc_|maOE2{B~S_DX7;UM zi>=RG{)MeSUbwrzT+d#9QT6#B=BaaQ8GY@Nh5latc|GX=N9HS^5BlAzEHDrC**if^ zeul&E`_>M^-D($hJTflH&foXvss8E>>&2BSy2bUcY1SW!o+eNf;q(4l%#qV#g7d7) z*G-+idPdskQ;ZA@Ju-KS552Q|9HXhHt9y0z!6iTLFP`gvvG05Ji~Zt%TFmP9&eWS- zXt?(8>#A4Q+vd-^bI~XNOW6f|@jo->?c3BDcJ?3R-10}4O*4povLeJ|gd_8aX%rg03@wX7a^TY0dyW?2@eox8Q5rQ+_c(v_>%?ea=* z%sJ7i;cw^a>T1Q#z+m@J#=5NM+xz?L_gLHCtA1}g%c@js>Xa!jWD=B|{v2eNztAIT zT;gXDSNAh@&l*++1_c@SKAAu#9;1mIjV-S~GlD`x!6f-u&&BKmeK%C^TNXnD{=-*Q zP~bH198!L_EcElTRX>@wo{2tcIqRC}%Rei%S6n@@M08SI;*vWh>t`J=eST4E^<*2v z?YfU&#<_l8W0(4S`GWszwgiM;l*#gV^+aG&&Wy!zN}H#x5{sQAts9yqfAIIiJ8`1# zciKOE-Su}z-}xK8bvci-R-Sqhe$4*3ZNbXtFN{5Ti_etpIg|N7ruoai-M=>Gp59#l zy^i(jcZC!0vi7evWSf(Gl5N)H%RiUD&5b_PV_LvdsrZ+5zVm~}UQMfx_XIACE?(*P zLTSN1_c`+gvKwEotDJwYK3?Bfr*zTAplwYnPM*ovUYzG$eR@UDY2%|CriRTGYE9Xg zx-kBZ%I&oaH*J#2s@~t5b*iM$(ekRfo_f*ZdBO3HXKMnU9yoQ$r|cu=_FBQ_Hjnmq zi<@65?K;qWE@e{E*=OHoX)gP`_ry}Yz!s&symHC83)`xqmv#7wF8#h!*=g>c-5Vo% zCY|()J-J}%QrD;2TOw3npZWMf==tr|8C|EXlGEypL$8FtF5K{UP43OlTNj)zQ_XxN zF?;Vm^MhsOZ_5QHTD{tEtKIgwQpo+p`{`nTZR8()Qa^Tld29bnF7gk4}T&fqy zdOg>32iv*wmttbU+49ztr}Vg-cs0#+g&&u8MRd^h6RA(w^v$ax(;u!`x16g^BV(KN z>7493Ev=5-<^?&{ZlzzJ{rJJAtn}eX-yR)yc;2g3p;j@v z8#WZ*mD(M}ed={t`}VmH3(}`VJDm?Yzrxr**Iq&9?bZD)Ti+^{$#-?H{_1;5^4^&e zr6bA@H?4aAdPDT2S4;Or23DrUz1^yQd6FJKk9e2DP1aLQuB((zG8N4K{_XCGv(sOj z&q&_Xnr-YEq@ExCb$V`%td-_S=lCyb;li#M591J^L{_gd#$v%_u_U_wV8bdvir~E?)WG*KRj-kuF8G8w~u#hv)TSOxqD%Ye30

1(a1&}%8J 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 literal 5550 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 \uicontrol Coco with which you + can set the directory in which Coco is installed. But in most cases, the default + settings need not to be changed. + + \section1 Measure code coverage + + With the Coco plugin, it is possible to set up code coverage easily for Qt Creator + projects that are built with QMake or CMake. + + The general idea is that you take an existing build configuration, like "Debug", + clone it with a new name (like "DebugCoverage") and then use the plugin to + configure it for the use with Coco. Switching back and forth between coverage + and normal builds on the same build configuration is not recommended. + + \section2 Components of the plugin + + With the plugin enabled, C/C++ projects get + \list + \li A project settings menu at \uicontrol Projects > \uicontrol {Project settings} + > \uicontrol {Coco Code Coverage} with which you can enable and configure + code coverage. + \li In the Build Settings, an additional pseudo build step that shows whether + code coverage was enabled. There is also a button to directly disable or + enable code coverage from this build step. + + \image qtcreator-coco-buildstep.png {Ficticious build step for code coverage} + \endlist + + If code coverage is enabled, the plugin generates a \e {settings} file that is read + by the build tool before the other configuration files and which changes the + build process so that the Coco compiler wrappers are used instead of the original + compiler. The settings file is always located in the root directory of the + project sources. It also contains the coverage flags and possible overrides and + can be checked in into version control to preserve the settings. + + \section2 The Project settings page + + \image qtcreator-coco-configpage.png {Settings page} + + The pages for QMake and CMake projects do not differ very much. They contain: + \list + \li A checkbox to enable and disable code coverage. + \li A field to enter code coverage options. There are no settings that are + needed to enable code coverage. Below the field are buttons: + \list + \li \uicontrol {Exclude file...} to exclude a file from instrumentation + more easily. + \li \uicontrol {Exclude directory...} to exclude a directory from + instrumentation more easily. + \li \uicontrol Override to open another entry field in which you can + enter additional commands at the end of the settings file. It is + meant for special cases in which the usual configuration flags are + not enough. + \endlist + \li A button \uicontrol Revert to reload the coverage settings from the current + settings file. + \li A button \uicontrol Save or \uicontrol {Save & Re-configure} to write + the settings to the settings file and reconfigure the project, if + necessary. + \li A list with the project build settings that were changed by the plugin. + \endlist + + \section2 QMake projects + + The settings file is \c {cocoplugin.prf}. It is a QMake "feature file". + + For a command line build, \c {qmake} must be run with the additional options + \tt {CONFIG+=cocoplugin COCOPATH=\e{}}. It is also necessary + to set the environment variable \c {QMAKEFEATURES} to the directory in which + \c {cocoplugin.prf} is located. + + \section2 CMake projects + + The settings file is \c {cocoplugin.cmake}. It is a CMake cache preload script. + Apart from this file, the "compiler files" \c {cocoplugin-gcc.cmake}, + \c {cocoplugin-clang.cmake} and \c {cocoplugin-visualstudio.cmake} are created + in the same directory. They are needed for a command line build. + + In a command-line build, run CMake in the form + "\tt {cmake \e{} -C\e{}/cocoplugin-gcc.cmake}" + (if you are compiling with GCC). The file \c {cocoplugin-gcc.cmake} includes + then \c {cocoplugin.cmake}. + + If you use a compiler different from GCC, clang or Visual Studio, one of the + compiler files must be modified for the new compiler. + + \section1 Check code coverage + + With the help of Coco CoverageBrowser, you can analyze the test coverage by + loading an instrumentation database (a \c .csmes file), which was generated by + Coco CoverageScanner. + + \section2 Configure Coco \list 1 \li Go to \uicontrol Analyze > \uicontrol {Squish Coco}. \image qtcreator-coco.png {Coco CoverageBrowser and CSMes file} - \li In \uicontrol {CoverageBrowser}, enter the path to the Coco - coverage browser to use for analyzing a .csmes file. \li In \uicontrol CSMes, select the instrumentation database to load. \li Select \uicontrol Open to start CoverageBrowser. \li In CoverageBrowser, go to \uicontrol File > - \uicontrol {Load Execution Report} and select the .csexe for the + \uicontrol {Load Execution Report} and select the \c .csexe file for the coverage scan. \image coco-coveragebrowser-load-execution-report.png {Load Execution Report dialog} \li To keep the execution report, clear @@ -39,7 +130,7 @@ after the code in \uicontrol Edit mode. You can change the fonts and colors used for different types of results. - \section1 Changing Fonts and Colors + \section2 Changing Fonts and Colors To change the default fonts and colors, go to \preferences > \uicontrol {Text Editor} > \uicontrol {Font & Colors}. diff --git a/src/plugins/cmakeprojectmanager/builddirparameters.h b/src/plugins/cmakeprojectmanager/builddirparameters.h index b73993eb37c..fc5c812bbd8 100644 --- a/src/plugins/cmakeprojectmanager/builddirparameters.h +++ b/src/plugins/cmakeprojectmanager/builddirparameters.h @@ -19,9 +19,11 @@ namespace ProjectExplorer { class Project; } -namespace CMakeProjectManager::Internal { - +namespace CMakeProjectManager { class CMakeBuildSystem; +} + +namespace CMakeProjectManager::Internal { class BuildDirParameters { diff --git a/src/plugins/cmakeprojectmanager/cmakebuildconfiguration.cpp b/src/plugins/cmakeprojectmanager/cmakebuildconfiguration.cpp index 1c5e78980ab..c11aa6be18a 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildconfiguration.cpp +++ b/src/plugins/cmakeprojectmanager/cmakebuildconfiguration.cpp @@ -117,6 +117,8 @@ public: void setError(const QString &message); void setWarning(const QString &message); + void updateInitialCMakeArguments(); + private: void updateButtonState(); void updateAdvancedCheckBox(); @@ -135,7 +137,6 @@ private: void batchEditConfiguration(); void reconfigureWithInitialParameters(); - void updateInitialCMakeArguments(); void kitCMakeConfiguration(); void updateConfigureDetailsWidgetsSummary( const QStringList &configurationArguments = QStringList()); @@ -1838,7 +1839,19 @@ QString CMakeBuildSystem::warning() const NamedWidget *CMakeBuildConfiguration::createConfigWidget() { - return new CMakeBuildSettingsWidget(this); + m_configWidget = new CMakeBuildSettingsWidget(this); + return m_configWidget; +} + +void CMakeBuildConfiguration::updateInitialCMakeArguments() +{ + Q_ASSERT(m_configWidget); + m_configWidget->updateInitialCMakeArguments(); +} + +QStringList CMakeBuildConfiguration::initialCMakeOptions() const +{ + return initialCMakeArguments.allValues(); } CMakeConfig CMakeBuildConfiguration::signingFlags() const diff --git a/src/plugins/cmakeprojectmanager/cmakebuildconfiguration.h b/src/plugins/cmakeprojectmanager/cmakebuildconfiguration.h index 8a7d2bace4d..398b42ffa50 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildconfiguration.h +++ b/src/plugins/cmakeprojectmanager/cmakebuildconfiguration.h @@ -14,10 +14,10 @@ namespace CMakeProjectManager { class CMakeProject; +class CMakeBuildSystem; namespace Internal { -class CMakeBuildSystem; class CMakeBuildSettingsWidget; class CMakeProjectImporter; @@ -75,7 +75,7 @@ public: void setRestrictedBuildTarget(const QString &buildTarget); Utils::Environment configureEnvironment() const; - Internal::CMakeBuildSystem *cmakeBuildSystem() const; + CMakeBuildSystem *cmakeBuildSystem() const; QStringList additionalCMakeArguments() const; void setAdditionalCMakeArguments(const QStringList &args); @@ -90,6 +90,9 @@ public: QtSupport::QmlDebuggingAspect qmlDebugging{this}; Internal::ConfigureEnvironmentAspect configureEnv{this, this}; + void updateInitialCMakeArguments(); + QStringList initialCMakeOptions() const; + signals: void signingFlagsChanged(); void configureEnvironmentChanged(); @@ -105,11 +108,12 @@ private: void setBuildPresetToBuildSteps(const ProjectExplorer::Target *target); void filterConfigArgumentsFromAdditionalCMakeArguments(); - Internal::CMakeBuildSystem *m_buildSystem = nullptr; + CMakeBuildSystem *m_buildSystem = nullptr; + Internal::CMakeBuildSettingsWidget *m_configWidget = nullptr; QStringList m_unrestrictedBuildTargets; friend class Internal::CMakeBuildSettingsWidget; - friend class Internal::CMakeBuildSystem; + friend class CMakeBuildSystem; }; class CMAKE_EXPORT CMakeBuildConfigurationFactory diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp index 3547e96ed10..3d8d75eaa78 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp @@ -62,8 +62,9 @@ using namespace ProjectExplorer; using namespace TextEditor; using namespace Utils; +using namespace CMakeProjectManager::Internal; -namespace CMakeProjectManager::Internal { +namespace CMakeProjectManager { static Q_LOGGING_CATEGORY(cmakeBuildSystemLog, "qtc.cmake.buildsystem", QtWarningMsg); @@ -2523,4 +2524,4 @@ ExtraCompiler *CMakeBuildSystem::findExtraCompiler(const ExtraCompilerFilter &fi return Utils::findOrDefault(m_extraCompilers, filter); } -} // CMakeProjectManager::Internal +} // CMakeProjectManager diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.h b/src/plugins/cmakeprojectmanager/cmakebuildsystem.h index 2b55097852d..2b6b3f79f2f 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.h +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.h @@ -29,13 +29,11 @@ namespace CMakeProjectManager { class CMakeBuildConfiguration; class CMakeProject; -namespace Internal { - // -------------------------------------------------------------------- // CMakeBuildSystem: // -------------------------------------------------------------------- -class CMakeBuildSystem final : public ProjectExplorer::BuildSystem +class CMAKE_EXPORT CMakeBuildSystem final : public ProjectExplorer::BuildSystem { Q_OBJECT @@ -157,7 +155,7 @@ private: Utils::FilePaths *); bool addTsFiles(ProjectExplorer::Node *context, const Utils::FilePaths &filePaths, Utils::FilePaths *); - bool renameFile(CMakeTargetNode *context, + bool renameFile(Internal::CMakeTargetNode *context, const Utils::FilePath &oldFilePath, const Utils::FilePath &newFilePath, bool &shouldRunCMake); @@ -175,10 +173,10 @@ private: }; void reparse(int reparseParameters); QString reparseParametersString(int reparseFlags); - void setParametersAndRequestParse(const BuildDirParameters ¶meters, + void setParametersAndRequestParse(const Internal::BuildDirParameters ¶meters, const int reparseParameters); - bool mustApplyConfigurationChangesArguments(const BuildDirParameters ¶meters) const; + bool mustApplyConfigurationChangesArguments(const Internal::BuildDirParameters ¶meters) const; // State handling: // Parser states: @@ -208,7 +206,7 @@ private: void wireUpConnections(); - void ensureBuildDirectory(const BuildDirParameters ¶meters); + void ensureBuildDirectory(const Internal::BuildDirParameters ¶meters); void stopParsingAndClearState(); void becameDirty(); @@ -243,7 +241,7 @@ private: ProjectExplorer::ProjectUpdater *m_cppCodeModelUpdater = nullptr; QList m_extraCompilers; QList m_buildTargets; - QSet m_cmakeFiles; + QSet m_cmakeFiles; QHash m_cmakeSymbolsHash; QHash m_dotCMakeFilesHash; QHash m_findPackagesFilesHash; @@ -254,9 +252,9 @@ private: QHash m_filesToBeRenamed; // Parsing state: - BuildDirParameters m_parameters; + Internal::BuildDirParameters m_parameters; int m_reparseParameters = REPARSE_DEFAULT; - FileApiReader m_reader; + Internal::FileApiReader m_reader; mutable bool m_isHandlingError = false; // CTest integration @@ -271,5 +269,4 @@ private: QString m_warning; }; -} // namespace Internal } // namespace CMakeProjectManager diff --git a/src/plugins/coco/CMakeLists.txt b/src/plugins/coco/CMakeLists.txt index bb5c5f165ac..71693fac7ad 100644 --- a/src/plugins/coco/CMakeLists.txt +++ b/src/plugins/coco/CMakeLists.txt @@ -1,7 +1,51 @@ add_qtc_plugin(Coco - PLUGIN_DEPENDS Core LanguageClient + PLUGIN_DEPENDS + QtCreator::Core + QtCreator::LanguageClient + QtCreator::ProjectExplorer + QtCreator::QmakeProjectManager + DEPENDS + QtCreator::CMakeProjectManager + QtCreator::ExtensionSystem SOURCES - cocolanguageclient.cpp cocolanguageclient.h + Coco.json.in + cocobuild/buildsettings.cpp + cocobuild/buildsettings.h + cocobuild/cmakemodificationfile.cpp + cocobuild/cmakemodificationfile.h + cocobuild/cococmakesettings.cpp + cocobuild/cococmakesettings.h + cocobuild/cocobuildstep.cpp + cocobuild/cocobuildstep.h + cocobuild/cocoprojectwidget.cpp + cocobuild/cocoprojectwidget.h + cocobuild/modificationfile.cpp + cocobuild/modificationfile.h + cocobuild/qmakefeaturefile.cpp + cocobuild/qmakefeaturefile.h + cocobuild/cocoqmakesettings.cpp + cocobuild/cocoqmakesettings.h + cocolanguageclient.cpp + cocolanguageclient.h cocoplugin.cpp + cocoplugin.qrc + cocoplugin_global.h + cocopluginconstants.h cocotr.h + common.cpp + common.h + files/cocoplugin-clang.cmake + files/cocoplugin-gcc.cmake + files/cocoplugin-visualstudio.cmake + files/cocoplugin.cmake + files/cocoplugin.prf + images/SquishCoco_48x48.png + settings/cocoinstallation.cpp + settings/cocoinstallation.h + settings/cocoprojectsettingswidget.cpp + settings/cocoprojectsettingswidget.h + settings/globalsettings.cpp + settings/globalsettings.h + settings/globalsettingspage.cpp + settings/globalsettingspage.h ) diff --git a/src/plugins/coco/Coco.json.in b/src/plugins/coco/Coco.json.in index 3c1b411b0b3..e9eb090aa42 100644 --- a/src/plugins/coco/Coco.json.in +++ b/src/plugins/coco/Coco.json.in @@ -15,9 +15,10 @@ "", "Alternatively, this plugin may be used under the terms of the GNU General Public License version 3 as published by the Free Software Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT included in the packaging of this plugin. Please review the following information to ensure the GNU General Public License requirements will be met: https://www.gnu.org/licenses/gpl-3.0.html." ], - "Description" : "Access the Coco CoverageBrowser.", + "Description" : "Configure Coco and access the results", "LongDescription" : [ - "View the results from the Coco CoverageBrowser to make tests more efficient and complete." + "Configure CMake and QMake projects for code coverage.", + "Highlight the source code according to the measured coverage." ], "Url" : "https://www.qt.io", "DocumentationUrl" : "https://doc.qt.io/qtcreator/creator-coco.html", diff --git a/src/plugins/coco/coco.qbs b/src/plugins/coco/coco.qbs index 6e32634eaf4..abfe8e939f8 100644 --- a/src/plugins/coco/coco.qbs +++ b/src/plugins/coco/coco.qbs @@ -5,14 +5,57 @@ QtcPlugin { Depends { name: "Core" } Depends { name: "LanguageClient" } + Depends { name: "CMakeProjectManager" } + Depends { name: "ExtensionSystem" } + Depends { name: "ProjectExplorer" } + Depends { name: "QmakeProjectManager" } Depends { name: "TextEditor" } + Depends { name: "Utils" } Depends { name: "Qt"; submodules: ["widgets"] } files: [ - "cocoplugin.cpp", + "cocobuild/buildsettings.cpp", + "cocobuild/buildsettings.h", + "cocobuild/cmakemodificationfile.cpp", + "cocobuild/cmakemodificationfile.h", + "cocobuild/cocobuildstep.cpp", + "cocobuild/cocobuildstep.h", + "cocobuild/cococmakesettings.cpp", + "cocobuild/cococmakesettings.h", + "cocobuild/cocoprojectwidget.cpp", + "cocobuild/cocoprojectwidget.h", + "cocobuild/cocoprojectwidget.ui", + "cocobuild/cocoqmakesettings.cpp", + "cocobuild/cocoqmakesettings.h", + "cocobuild/modificationfile.cpp", + "cocobuild/modificationfile.h", + "cocobuild/qmakefeaturefile.cpp", + "cocobuild/qmakefeaturefile.h", "cocolanguageclient.cpp", "cocolanguageclient.h", + "cocoplugin.cpp", + "cocoplugin.qrc", + "cocoplugin_global.h", + "cocopluginconstants.h", + "cocotr.h", + "common.cpp", + "common.h", + "files/cocoplugin-clang.cmake", + "files/cocoplugin-gcc.cmake", + "files/cocoplugin-visualstudio.cmake", + "files/cocoplugin.cmake", + "files/cocoplugin.prf", + "images/SquishCoco_48x48.png", + "settings/cocoinstallation.cpp", + "settings/cocoinstallation.h", + "settings/cocoprojectsettingswidget.cpp", + "settings/cocoprojectsettingswidget.h", + "settings/globalsettings.cpp", + "settings/globalsettings.h", + "settings/globalsettingspage.cpp", + "settings/globalsettingspage.h", + "settings/globalsettingspage.ui", ] } diff --git a/src/plugins/coco/cocobuild/buildsettings.cpp b/src/plugins/coco/cocobuild/buildsettings.cpp new file mode 100644 index 00000000000..c905b6806d1 --- /dev/null +++ b/src/plugins/coco/cocobuild/buildsettings.cpp @@ -0,0 +1,100 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "buildsettings.h" + +#include "cocobuildstep.h" +#include "cococmakesettings.h" +#include "cocoqmakesettings.h" +#include "modificationfile.h" + +#include +#include +#include + +namespace Coco::Internal { + +bool BuildSettings::supportsBuildConfig(const ProjectExplorer::BuildConfiguration &config) +{ + return config.id() == QmakeProjectManager::Constants::QMAKE_BC_ID + || config.id() == CMakeProjectManager::Constants::CMAKE_BUILDCONFIGURATION_ID; +} + +BuildSettings *BuildSettings::createdFor(const ProjectExplorer::BuildConfiguration &config) +{ + if (config.id() == QmakeProjectManager::Constants::QMAKE_BC_ID) + return new CocoQMakeSettings{config.project()}; + else if (config.id() == CMakeProjectManager::Constants::CMAKE_BUILDCONFIGURATION_ID) + return new CocoCMakeSettings{config.project()}; + else + return nullptr; +} + +BuildSettings::BuildSettings(ModificationFile &featureFile, ProjectExplorer::Project *project) + : m_featureFile{featureFile} + , m_project{*project} +{ + // Do not use m_featureFile in the constructor; it may not yet be valid. +} + +void BuildSettings::connectToBuildStep(CocoBuildStep *step) const +{ + connect( + activeTarget(), + &ProjectExplorer::Target::buildSystemUpdated, + step, + &CocoBuildStep::buildSystemUpdated); +} + +bool BuildSettings::enabled() const +{ + return m_enabled; +} + +const QStringList &BuildSettings::options() const +{ + return m_featureFile.options(); +} + +const QStringList &BuildSettings::tweaks() const +{ + return m_featureFile.tweaks(); +} + +bool BuildSettings::hasTweaks() const +{ + return !m_featureFile.tweaks().isEmpty(); +} + +QString BuildSettings::featureFilenName() const +{ + return m_featureFile.fileName(); +} + +QString BuildSettings::featureFilePath() const +{ + return m_featureFile.nativePath(); +} + +void BuildSettings::provideFile() +{ + if (!m_featureFile.exists()) + write("", ""); +} + +QString BuildSettings::tableRow(const QString &name, const QString &value) const +{ + return QString("%1%2").arg(name, value); +} + +void BuildSettings::setEnabled(bool enabled) +{ + m_enabled = enabled; +} + +ProjectExplorer::Target *BuildSettings::activeTarget() const +{ + return m_project.activeTarget(); +} + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/buildsettings.h b/src/plugins/coco/cocobuild/buildsettings.h new file mode 100644 index 00000000000..10cf8e96925 --- /dev/null +++ b/src/plugins/coco/cocobuild/buildsettings.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include + +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +namespace ProjectExplorer { +class BuildConfiguration; +class Project; +class Target; +} + +namespace Coco::Internal { + +class CocoBuildStep; +class CocoProjectWidget; +class ModificationFile; + +class BuildSettings : public QObject +{ + Q_OBJECT +public: + static bool supportsBuildConfig(const ProjectExplorer::BuildConfiguration &config); + static BuildSettings *createdFor(const ProjectExplorer::BuildConfiguration &config); + + explicit BuildSettings(ModificationFile &featureFile, ProjectExplorer::Project *project); + virtual ~BuildSettings() {} + + void connectToBuildStep(CocoBuildStep *step) const; + virtual void connectToProject(CocoProjectWidget *) const {} + virtual void read() = 0; + bool enabled() const; + virtual bool validSettings() const = 0; + virtual void setCoverage(bool on) = 0; + + virtual QString saveButtonText() const = 0; + virtual void reconfigure() {}; + virtual void stopReconfigure() {}; + virtual bool needsReconfigure() const { return false; } + + virtual QString configChanges() const = 0; + + const QStringList &options() const; + const QStringList &tweaks() const; + virtual QString projectDirectory() const = 0; + + bool hasTweaks() const; + QString featureFilenName() const; + QString featureFilePath() const; + + virtual void write(const QString &options, const QString &tweaks) = 0; + void provideFile(); + +protected: + QString tableRow(const QString &name, const QString &value) const; + void setEnabled(bool enabled); + ProjectExplorer::Target *activeTarget() const; + +private: + ModificationFile &m_featureFile; + ProjectExplorer::Project &m_project; + bool m_enabled = false; +}; + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/cmakemodificationfile.cpp b/src/plugins/coco/cocobuild/cmakemodificationfile.cpp new file mode 100644 index 00000000000..9813e92be0c --- /dev/null +++ b/src/plugins/coco/cocobuild/cmakemodificationfile.cpp @@ -0,0 +1,91 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "cmakemodificationfile.h" + +#include "cocopluginconstants.h" + +#include +#include +#include + +namespace Coco::Internal { + +using namespace ProjectExplorer; + +static const char flagsSetting[] = "set(coverage_flags_list\n"; +static const char tweaksLine[] = "# User-supplied settings follow here:\n"; + +CMakeModificationFile::CMakeModificationFile(Project *project) + : m_project{project} +{} + +QString CMakeModificationFile::fileName() const +{ + return QString(Constants::PROFILE_NAME) + ".cmake"; +} + +void CMakeModificationFile::setProjectDirectory(const Utils::FilePath &projectDirectory) +{ + setFilePath(projectDirectory.pathAppended(fileName())); +} + +QStringList CMakeModificationFile::defaultModificationFile() const +{ + return contentOf(":/cocoplugin/files/cocoplugin.cmake"); +} + +void CMakeModificationFile::read() +{ + clear(); + QStringList file = currentModificationFile(); + + { + QStringList options; + int i = file.indexOf(flagsSetting); + if (i != -1) { + i++; + while (i < file.size() && !file[i].startsWith(')')) { + options += file[i].trimmed(); + i++; + } + } + setOptions(options); + } + { + QStringList tweaks; + int i = file.indexOf(tweaksLine); + if (i != -1) { + i++; + while (i < file.size()) { + tweaks += file[i].chopped(1); + i++; + } + } + setTweaks(tweaks); + } +} + +void CMakeModificationFile::write() const +{ + QFile out(nativePath()); + out.open(QIODevice::WriteOnly | QIODevice::Text); + + QTextStream outStream(&out); + for (QString &line : defaultModificationFile()) { + outStream << line; + + if (line.startsWith(flagsSetting)) { + for (const QString &option : options()) { + QString line = " " + option + '\n'; + outStream << line; + } + } + } + for (const QString &line : tweaks()) + outStream << line << "\n"; + + out.close(); +} + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/cmakemodificationfile.h b/src/plugins/coco/cocobuild/cmakemodificationfile.h new file mode 100644 index 00000000000..485c5fa4326 --- /dev/null +++ b/src/plugins/coco/cocobuild/cmakemodificationfile.h @@ -0,0 +1,32 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "modificationfile.h" + +namespace ProjectExplorer { +class Project; +} + +namespace Coco::Internal { + +class CMakeModificationFile : public ModificationFile +{ +public: + CMakeModificationFile(ProjectExplorer::Project *project); + + void read() override; + void write() const override; + + QString fileName() const override; + void setProjectDirectory(const Utils::FilePath &projectDirectory) override; + +protected: + QStringList defaultModificationFile() const override; + +private: + ProjectExplorer::Project *m_project; +}; + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/cocobuildstep.cpp b/src/plugins/coco/cocobuild/cocobuildstep.cpp new file mode 100644 index 00000000000..6a71157e7b6 --- /dev/null +++ b/src/plugins/coco/cocobuild/cocobuildstep.cpp @@ -0,0 +1,126 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "cocobuildstep.h" + +#include "cocopluginconstants.h" +#include "cocotr.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace Coco::Internal { + +using namespace ProjectExplorer; + +QMakeStepFactory::QMakeStepFactory() +{ + registerStep(Utils::Id{Constants::COCO_STEP_ID}); + setSupportedProjectType(QmakeProjectManager::Constants::QMAKEPROJECT_ID); + setSupportedStepList(ProjectExplorer::Constants::BUILDSTEPS_BUILD); + setRepeatable(false); +} + +CMakeStepFactory::CMakeStepFactory() +{ + registerStep(Utils::Id{Constants::COCO_STEP_ID}); + setSupportedProjectType(CMakeProjectManager::Constants::CMAKE_PROJECT_ID); + setSupportedStepList(ProjectExplorer::Constants::BUILDSTEPS_BUILD); + setRepeatable(false); +} + +CocoBuildStep *CocoBuildStep::create(BuildConfiguration *buildConfig) +{ + // The "new" command creates a small memory leak which we can tolerate. + return new CocoBuildStep( + new BuildStepList(buildConfig, Constants::COCO_STEP_ID), Utils::Id(Constants::COCO_STEP_ID)); +} + +CocoBuildStep::CocoBuildStep(ProjectExplorer::BuildStepList *bsl, Utils::Id id) + : BuildStep(bsl, id) + , m_reconfigureButton{new QPushButton} +{} + +bool CocoBuildStep::init() +{ + return true; +} + +void CocoBuildStep::buildSystemUpdated() +{ + updateDisplay(); +} + +void CocoBuildStep::onReconfigureButtonClicked() +{ + m_valid = !m_valid; + + setSummaryText(Tr::tr("Coco Code Coverage: Reconfiguring...")); + m_reconfigureButton->setEnabled(false); + m_buildSettings->setCoverage(m_valid); + m_buildSettings->provideFile(); + m_buildSettings->reconfigure(); +} + +QWidget *CocoBuildStep::createConfigWidget() +{ + connect( + m_reconfigureButton, + &QPushButton::clicked, + this, + &CocoBuildStep::onReconfigureButtonClicked); + + Layouting::Form builder; + builder.addRow({m_reconfigureButton, new QLabel}); + builder.setNoMargins(); + + return builder.emerge(); +} + +void CocoBuildStep::updateDisplay() +{ + CocoInstallation coco; + if (!coco.isValid()) { + setSummaryText("" + Tr::tr("Coco Code Coverage: No working Coco installation") + ""); + m_reconfigureButton->setEnabled(false); + return; + } + + m_valid = m_buildSettings->validSettings(); + + if (m_valid) { + setSummaryText("" + Tr::tr("Coco Code Coverage: Enabled") + ""); + m_reconfigureButton->setText(Tr::tr("Disable Coverage")); + } else { + setSummaryText(Tr::tr("Coco Code Coverage: Disabled")); + m_reconfigureButton->setText(Tr::tr("Enable Coverage")); + } + + m_reconfigureButton->setEnabled(true); +} + +void CocoBuildStep::display(BuildConfiguration *buildConfig) +{ + Q_ASSERT( m_buildSettings.isNull() ); + + m_buildSettings = BuildSettings::createdFor(*buildConfig); + m_buildSettings->read(); + m_buildSettings->connectToBuildStep(this); + + setImmutable(true); + updateDisplay(); +} + +Tasking::GroupItem CocoBuildStep::runRecipe() +{ + return Tasking::GroupItem({}); +} + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/cocobuildstep.h b/src/plugins/coco/cocobuild/cocobuildstep.h new file mode 100644 index 00000000000..15b48565fbc --- /dev/null +++ b/src/plugins/coco/cocobuild/cocobuildstep.h @@ -0,0 +1,59 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "buildsettings.h" + +#include +#include +#include + +#include + +class QPushButton; + +namespace Coco::Internal { + +class QMakeStepFactory: public ProjectExplorer::BuildStepFactory +{ +public: + QMakeStepFactory(); +}; + +class CMakeStepFactory: public ProjectExplorer::BuildStepFactory +{ +public: + CMakeStepFactory(); +}; + +class CocoBuildStep : public ProjectExplorer::BuildStep +{ + Q_OBJECT +public: + static CocoBuildStep *create(ProjectExplorer::BuildConfiguration *buildConfig); + + CocoBuildStep(ProjectExplorer::BuildStepList *bsl, Utils::Id id); + + bool init() override; + void display(ProjectExplorer::BuildConfiguration *buildConfig); + +public slots: + void buildSystemUpdated(); + +private slots: + void onReconfigureButtonClicked(); + +protected: + QWidget *createConfigWidget() override; + +private: + void updateDisplay(); + Tasking::GroupItem runRecipe() override; + + QPointer m_buildSettings; + bool m_valid; + QPushButton *m_reconfigureButton; +}; + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/cococmakesettings.cpp b/src/plugins/coco/cocobuild/cococmakesettings.cpp new file mode 100644 index 00000000000..ceb3ec8012a --- /dev/null +++ b/src/plugins/coco/cocobuild/cococmakesettings.cpp @@ -0,0 +1,167 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "cococmakesettings.h" + +#include "cocobuild/cocoprojectwidget.h" +#include "cocotr.h" +#include "common.h" + +#include +#include +#include + +using namespace ProjectExplorer; +using namespace CMakeProjectManager; + +namespace Coco::Internal { + +CocoCMakeSettings::CocoCMakeSettings(Project *project) + : BuildSettings{m_featureFile, project} + , m_featureFile{project} +{} + +CocoCMakeSettings::~CocoCMakeSettings() {} + +void CocoCMakeSettings::connectToProject(CocoProjectWidget *parent) const +{ + connect( + activeTarget(), &Target::buildSystemUpdated, parent, &CocoProjectWidget::buildSystemUpdated); + connect( + qobject_cast(activeTarget()->buildSystem()), + &CMakeProjectManager::CMakeBuildSystem::errorOccurred, + parent, + &CocoProjectWidget::configurationErrorOccurred); +} + +void CocoCMakeSettings::read() +{ + setEnabled(false); + if (Target *target = activeTarget()) { + if ((m_buildConfig = qobject_cast( + target->activeBuildConfiguration()))) { + m_featureFile.setProjectDirectory(m_buildConfig->project()->projectDirectory()); + m_featureFile.read(); + setEnabled(true); + } + } +} + +QString CocoCMakeSettings::initialCacheOption() const +{ + return QString("-C%1").arg(m_featureFile.nativePath()); +} + +bool CocoCMakeSettings::hasInitialCacheOption(const QStringList &args) const +{ + for (int i = 0; i < args.length(); ++i) { + if (args[i] == "-C" && i + 1 < args.length() && args[i + 1] == m_featureFile.nativePath()) + return true; + + if (args[i] == initialCacheOption()) + return true; + } + + return false; +} + +bool CocoCMakeSettings::validSettings() const +{ + return enabled() && m_featureFile.exists() + && hasInitialCacheOption(m_buildConfig->additionalCMakeArguments()); +} + +void CocoCMakeSettings::setCoverage(bool on) +{ + if (!enabled()) + return; + + auto values = m_buildConfig->initialCMakeOptions(); + QStringList args = Utils::filtered(values, [&](const QString &option) { + return !(option.startsWith("-C") && option.endsWith(featureFilenName())); + }); + + if (on) + args << QString("-C%1").arg(m_featureFile.nativePath()); + + m_buildConfig->setInitialCMakeArguments(args); +} + +QString CocoCMakeSettings::saveButtonText() const +{ + return Tr::tr("Save && Re-configure"); +} + +QString CocoCMakeSettings::configChanges() const +{ + return "" + + tableRow("Additional CMake options: ", maybeQuote(initialCacheOption())) + + tableRow("Initial cache script: ", maybeQuote(featureFilePath())) + "
"; +} + +void CocoCMakeSettings::reconfigure() +{ + if (!enabled()) + return; + + m_buildConfig->cmakeBuildSystem()->clearCMakeCache(); + m_buildConfig->updateInitialCMakeArguments(); + m_buildConfig->cmakeBuildSystem()->runCMake(); +} + +void Coco::Internal::CocoCMakeSettings::stopReconfigure() +{ + if (enabled()) + m_buildConfig->cmakeBuildSystem()->stopCMakeRun(); +} + +QString CocoCMakeSettings::projectDirectory() const +{ + if (enabled()) + return m_buildConfig->project()->projectDirectory().path(); + else + return ""; +} + +void CocoCMakeSettings::write(const QString &options, const QString &tweaks) +{ + m_featureFile.setOptions(options); + m_featureFile.setTweaks(tweaks); + m_featureFile.write(); + + writeToolchainFile(":/cocoplugin/files/cocoplugin-gcc.cmake"); + writeToolchainFile(":/cocoplugin/files/cocoplugin-clang.cmake"); + writeToolchainFile(":/cocoplugin/files/cocoplugin-visualstudio.cmake"); +} + +void CocoCMakeSettings::writeToolchainFile(const QString &internalPath) +{ + const Utils::FilePath projectDirectory = m_buildConfig->project()->projectDirectory(); + + QFile internalFile{internalPath}; + internalFile.open(QIODeviceBase::ReadOnly); + const QByteArray internalContent = internalFile.readAll(); + + const QString fileName = Utils::FilePath::fromString(internalPath).fileName(); + const Utils::FilePath toolchainPath{projectDirectory.pathAppended(fileName)}; + const QString toolchainNative = toolchainPath.nativePath(); + + if (toolchainPath.exists()) { + QFile currentFile{toolchainNative}; + currentFile.open(QIODeviceBase::ReadOnly); + + QByteArray currentContent = currentFile.readAll(); + if (internalContent == currentContent) + return; + + logSilently(Tr::tr("Overwrite file %1").arg(maybeQuote(toolchainNative))); + } else + logSilently(Tr::tr("Write file %1").arg(maybeQuote(toolchainNative))); + + QFile out{toolchainNative}; + out.open(QIODeviceBase::WriteOnly); + out.write(internalContent); + out.close(); +} + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/cococmakesettings.h b/src/plugins/coco/cocobuild/cococmakesettings.h new file mode 100644 index 00000000000..8535744b9f7 --- /dev/null +++ b/src/plugins/coco/cocobuild/cococmakesettings.h @@ -0,0 +1,51 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "cocobuild/buildsettings.h" +#include "cocobuild/cmakemodificationfile.h" + +#include +#include + +namespace CMakeProjectManager { +class CMakeBuildConfiguration; +class CMakeConfig; +} + +namespace Coco::Internal { + +class CocoProjectWidget; + +class CocoCMakeSettings : public BuildSettings +{ + Q_OBJECT +public: + explicit CocoCMakeSettings(ProjectExplorer::Project *project); + ~CocoCMakeSettings() override; + + void connectToProject(CocoProjectWidget *parent) const override; + void read() override; + bool validSettings() const override; + void setCoverage(bool on) override; + + QString saveButtonText() const override; + QString configChanges() const override; + bool needsReconfigure() const override { return true; } + void reconfigure() override; + void stopReconfigure() override; + + QString projectDirectory() const override; + void write(const QString &options, const QString &tweaks) override; + +private: + bool hasInitialCacheOption(const QStringList &args) const; + QString initialCacheOption() const; + void writeToolchainFile(const QString &internalPath); + + CMakeProjectManager::CMakeBuildConfiguration *m_buildConfig; + CMakeModificationFile m_featureFile; +}; + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/cocoprojectwidget.cpp b/src/plugins/coco/cocobuild/cocoprojectwidget.cpp new file mode 100644 index 00000000000..7cf55610eba --- /dev/null +++ b/src/plugins/coco/cocobuild/cocoprojectwidget.cpp @@ -0,0 +1,336 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "cocoprojectwidget.h" + +#include "buildsettings.h" +#include "cocopluginconstants.h" +#include "cocotr.h" +#include "common.h" +#include "settings/globalsettingspage.h" + +#include +#include + +#include +#include + +using namespace ProjectExplorer; +using namespace Core; + +namespace Coco::Internal { + +CocoProjectWidget::CocoProjectWidget(Project *project, const BuildConfiguration &buildConfig) + : m_project{project} + , m_buildConfigurationName{buildConfig.displayName()} +{ + using namespace Layouting; + using namespace Utils; + + m_configerrorLabel.setVisible(false); + m_configerrorLabel.setIconType(InfoLabel::Error); + Label docLink( + QString( + "%1") + .arg(Tr::tr("Documentation"))); + docLink.setOpenExternalLinks(true); + m_optionEdit.setDisplayStyle(StringAspect::TextEditDisplay); + m_tweaksEdit.setDisplayStyle(StringAspect::TextEditDisplay); + m_revertButton.setText(Tr::tr("Revert")); + QFont bold; + bold.setBold(true); + m_saveButton.setFont(bold); + + m_coverageGroupbox + = {groupChecker(m_coverageGroupBoxEnabled.groupChecker()), + Column{ + Row{Tr::tr("CoverageScanner Options"), st, docLink}, + m_optionEdit, + Row{PushButton{ + text(Tr::tr("Exclude File...")), + onClicked([&] { onExcludeFileButtonClicked(); }, this)}, + PushButton{ + text(Tr::tr("Exclude Directory...")), + onClicked([&] { onExcludeDirButtonClicked(); }, this)}, + m_tweaksButton, + st}, + m_tweaksDescriptionLabel, + m_tweaksEdit, + Row{Tr::tr("These settings are stored in"), m_fileNameLabel, st}, + Group{title(Tr::tr("Changed Build Settings")), Column{m_changesText}}}}; + + Column{ + m_configerrorLabel, + m_coverageGroupbox, + Row{m_messageLabel, st, &m_revertButton, &m_saveButton}} + .attachTo(this); + + m_buildSettings = BuildSettings::createdFor(buildConfig); + m_buildSettings->connectToProject(this); + + readSelectionDir(); + reloadSettings(); + + m_fileNameLabel.setValue(m_buildSettings->featureFilePath()); + m_tweaksDescriptionLabel.setText( + Tr::tr("Code for the end of the file \"%1\" to override the built-in declarations." + " Only needed in special cases.") + .arg(m_buildSettings->featureFilenName())); + setTweaksVisible(m_buildSettings->hasTweaks()); + clearMessageLabel(); + + connect(&m_coverageGroupBoxEnabled, &BoolAspect::changed, this, &CocoProjectWidget::onCoverageGroupBoxClicked); + + connect(&m_optionEdit, &StringAspect::changed, this, &CocoProjectWidget::onTextChanged); + connect(&m_tweaksEdit, &StringAspect::changed, this, &CocoProjectWidget::onTextChanged); + m_tweaksButton.onClicked([&] { onTweaksButtonClicked(); }, this); + + connect(&m_revertButton, &QPushButton::clicked, this, &CocoProjectWidget::onRevertButtonClicked); + connect(&m_saveButton, &QPushButton::clicked, this, &CocoProjectWidget::onSaveButtonClicked); + + connect(GlobalSettingsPage::instance().widget(), &GlobalSettingsWidget::updateCocoDir, this, &CocoProjectWidget::reloadSettings); +} + +// Read the build settings again and show them in the widget. +void CocoProjectWidget::reloadSettings() +{ + m_buildSettings->read(); + m_coverageGroupBoxEnabled.setValue(m_buildSettings->validSettings(), Utils::BaseAspect::BeQuiet); + m_coverageGroupbox.setTitle( + Tr::tr("Enable code coverage for build configuration \"%1\"").arg(m_buildConfigurationName)); + + m_optionEdit.setValue(m_buildSettings->options().join('\n'), Utils::BaseAspect::BeQuiet); + m_tweaksEdit.setValue(m_buildSettings->tweaks().join('\n'), Utils::BaseAspect::BeQuiet); + + setState(configDone); + displayChanges(); + + const bool valid = m_coco.isValid(); + m_configerrorLabel.setVisible(!valid); + if (!valid) { + m_configerrorLabel.setText( + Tr::tr("Coco is not installed correctly: \"%1\"").arg(m_coco.errorMessage())); + } +} + +void CocoProjectWidget::showEvent(QShowEvent *event) +{ + Q_UNUSED(event) + reloadSettings(); +} + +void CocoProjectWidget::buildSystemUpdated(ProjectExplorer::BuildSystem *bs) +{ + QString newBuildConfigurationName = bs->buildConfiguration()->displayName(); + + if (m_buildConfigurationName != newBuildConfigurationName) { + m_buildConfigurationName = newBuildConfigurationName; + logSilently(Tr::tr("Build Configuration changed to %1.").arg(newBuildConfigurationName)); + reloadSettings(); + } else if (m_configState == configRunning) + setState(configDone); +} + +void CocoProjectWidget::configurationErrorOccurred(const QString &error) +{ + Q_UNUSED(error) + + if (m_configState == configEdited) { + setMessageLabel(Utils::InfoLabel::Information, Tr::tr("Re-configuring stopped by user.")); + setState(configStopped); + } else { + // The variable error seems to contain no usable information. + setMessageLabel( + Utils::InfoLabel::Error, + Tr::tr("Error when configuring with \"%1\". " + "Check General Messages for more information.") + .arg(m_buildSettings->featureFilenName())); + setState(configDone); + } +} + +void CocoProjectWidget::setState(ConfigurationState state) +{ + m_configState = state; + + switch (m_configState) { + case configDone: + m_saveButton.setText(m_buildSettings->saveButtonText()); + m_saveButton.setEnabled(false); + m_revertButton.setEnabled(false); + break; + case configEdited: + m_saveButton.setText(m_buildSettings->saveButtonText()); + m_saveButton.setEnabled(true); + m_revertButton.setEnabled(true); + break; + case configRunning: + m_saveButton.setText(Tr::tr("Stop Re-configuring")); + m_saveButton.setEnabled(true); + m_revertButton.setEnabled(false); + break; + case configStopped: + m_saveButton.setText(Tr::tr("Re-configure")); + m_saveButton.setEnabled(true); + m_revertButton.setEnabled(false); + break; + } +} + +void CocoProjectWidget::readSelectionDir() +{ + QVariantMap settings = m_project->namedSettings(Constants::SETTINGS_NAME_KEY).toMap(); + + if (settings.contains(Constants::SELECTION_DIR_KEY)) + m_selectionDirectory = settings[Constants::SELECTION_DIR_KEY].toString(); + else + m_selectionDirectory = m_buildSettings->projectDirectory(); +} + +void CocoProjectWidget::writeSelectionDir(const QString &path) +{ + m_selectionDirectory = path; + + QVariantMap settings; + settings[Constants::SELECTION_DIR_KEY] = path; + + m_project->setNamedSettings(Constants::SETTINGS_NAME_KEY, settings); +} + +void CocoProjectWidget::setTweaksVisible(bool on) +{ + if (on) + m_tweaksButton.setText(Tr::tr("Override <<")); + else + m_tweaksButton.setText(Tr::tr("Override >>")); + + m_tweaksDescriptionLabel.setVisible(on); + m_tweaksEdit.setVisible(on); +} + +void CocoProjectWidget::setMessageLabel(const Utils::InfoLabel::InfoType type, const QString &text) +{ + m_messageLabel.setText(text); + m_messageLabel.setIconType(type); +} + +void CocoProjectWidget::clearMessageLabel() +{ + m_messageLabel.setText(""); + m_messageLabel.setIconType(Utils::InfoLabel::None); +} + +void Internal::CocoProjectWidget::onCoverageGroupBoxClicked() +{ + bool checked = m_coverageGroupBoxEnabled(); + + displayChanges(); + + if (!checked) { + m_buildSettings->setCoverage(false); + setState(configEdited); + return; + } + + if (!m_coco.isValid()) { + m_coverageGroupBoxEnabled.setValue(false, Utils::BaseAspect::BeQuiet); + + QMessageBox box; + box.setIcon(QMessageBox::Critical); + box.setText(Tr::tr("The Coco installation path is not set correctly.")); + box.addButton(QMessageBox::Cancel); + QPushButton *editButton = box.addButton(Tr::tr("Edit"), QMessageBox::AcceptRole); + box.exec(); + + if (box.clickedButton() == editButton) + Core::ICore::showOptionsDialog(Constants::COCO_SETTINGS_PAGE_ID); + + m_coverageGroupBoxEnabled.setValue(m_coco.isValid(), Utils::BaseAspect::BeQuiet); + } else + m_buildSettings->setCoverage(checked); + + setState(configEdited); +} + +void CocoProjectWidget::onSaveButtonClicked() +{ + if (m_configState == configRunning) { + logSilently(Tr::tr("Stop re-configuring")); + m_buildSettings->stopReconfigure(); + setState(configEdited); + return; + } + + QString options = m_optionEdit(); + QString tweaks = m_tweaksEdit(); + clearMessageLabel(); + + logSilently(Tr::tr("Write file \"%1\"").arg(m_buildSettings->featureFilePath())); + m_buildSettings->write(options, tweaks); + + if (m_buildSettings->needsReconfigure()) { + logSilently(Tr::tr("Re-configure")); + setState(configRunning); + m_buildSettings->reconfigure(); + } else + setState(configDone); +} + +void CocoProjectWidget::onRevertButtonClicked() +{ + clearMessageLabel(); + logSilently(Tr::tr("Reload file \"%1\"").arg(m_buildSettings->featureFilePath())); + reloadSettings(); +} + +void CocoProjectWidget::onTextChanged() +{ + setState(configEdited); +} + +void CocoProjectWidget::displayChanges() +{ + m_changesText.setValue(m_buildSettings->configChanges()); +} + +void CocoProjectWidget::addCocoOption(QString option) +{ + m_optionEdit.setValue(m_optionEdit() + "\n" + option); +} + +void CocoProjectWidget::onExcludeFileButtonClicked() +{ + QString fileName = QFileDialog::getOpenFileName( + this, Tr::tr("File to Exclude from Instrumentation"), m_selectionDirectory); + if (fileName.isEmpty()) + return; + + const auto fileNameInfo = Utils::FilePath::fromString(fileName); + addCocoOption("--cs-exclude-file-abs-wildcard=" + maybeQuote("*/" + fileNameInfo.fileName())); + + writeSelectionDir(fileNameInfo.path()); +} + +void CocoProjectWidget::onExcludeDirButtonClicked() +{ + QString path = QFileDialog::getExistingDirectory( + this, Tr::tr("Directory to Exclude from Instrumentation"), m_selectionDirectory); + if (path.isEmpty()) + return; + + const QString projectDir = m_buildSettings->projectDirectory(); + if (path.startsWith(projectDir)) + // Make it a relative path with "*/" at the beginnig. + path = "*/" + path.arg(path.mid(projectDir.size())); + + addCocoOption("--cs-exclude-file-abs-wildcard=" + maybeQuote(path)); + + writeSelectionDir(path); +} + +void Internal::CocoProjectWidget::onTweaksButtonClicked() +{ + setTweaksVisible(!m_tweaksEdit.isVisible()); +} + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/cocoprojectwidget.h b/src/plugins/coco/cocobuild/cocoprojectwidget.h new file mode 100644 index 00000000000..b7f2cf4e12c --- /dev/null +++ b/src/plugins/coco/cocobuild/cocoprojectwidget.h @@ -0,0 +1,81 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "buildsettings.h" +#include "projectexplorer/buildconfiguration.h" +#include "settings/cocoinstallation.h" +#include +#include + +#include +#include +#include + +namespace ProjectExplorer { +class Project; +} + +namespace Coco::Internal { + +class CocoProjectWidget : public QWidget +{ + Q_OBJECT +public: + enum ConfigurationState { configDone, configEdited, configRunning, configStopped }; + + explicit CocoProjectWidget( + ProjectExplorer::Project *project, const ProjectExplorer::BuildConfiguration &buildConfig); + +protected: + void showEvent(QShowEvent *event) override; + +public slots: + void buildSystemUpdated(ProjectExplorer::BuildSystem *bs); + void configurationErrorOccurred(const QString &error); + +private slots: + void onCoverageGroupBoxClicked(); + + void onSaveButtonClicked(); + void onRevertButtonClicked(); + void onExcludeFileButtonClicked(); + void onExcludeDirButtonClicked(); + void onTweaksButtonClicked(); + + void onTextChanged(); + +private: + void displayChanges(); + void reloadSettings(); + void addCocoOption(QString option); + void setState(ConfigurationState state); + void readSelectionDir(); + void writeSelectionDir(const QString &path); + void setTweaksVisible(bool on); + void setMessageLabel(const Utils::InfoLabel::InfoType type, const QString &text); + void clearMessageLabel(); + + Utils::TextDisplay m_configerrorLabel; + Utils::BoolAspect m_coverageGroupBoxEnabled; + Layouting::Group m_coverageGroupbox{}; + Utils::StringAspect m_optionEdit; + Layouting::PushButton m_tweaksButton{}; + Utils::TextDisplay m_tweaksDescriptionLabel; + Utils::StringAspect m_tweaksEdit; + Utils::StringAspect m_fileNameLabel; + Utils::TextDisplay m_messageLabel; + QPushButton m_revertButton; + QPushButton m_saveButton; + Utils::StringAspect m_changesText; + + ProjectExplorer::Project *m_project; + QPointer m_buildSettings; + QString m_selectionDirectory; + ConfigurationState m_configState = configDone; + QString m_buildConfigurationName; + CocoInstallation m_coco; +}; + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/cocoqmakesettings.cpp b/src/plugins/coco/cocobuild/cocoqmakesettings.cpp new file mode 100644 index 00000000000..c86b53ed2f7 --- /dev/null +++ b/src/plugins/coco/cocobuild/cocoqmakesettings.cpp @@ -0,0 +1,188 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "cocoqmakesettings.h" + +#include "cocobuild/cocoprojectwidget.h" +#include "cocopluginconstants.h" +#include "cocotr.h" +#include "common.h" + +#include +#include +#include +#include +#include + +using namespace ProjectExplorer; + +namespace Coco::Internal { + +CocoQMakeSettings::CocoQMakeSettings(Project *project) + : BuildSettings{m_featureFile, project} +{} + +CocoQMakeSettings::~CocoQMakeSettings() {} + +void CocoQMakeSettings::read() +{ + setEnabled(false); + if (Target *target = activeTarget()) { + if ((m_buildConfig = qobject_cast(target->activeBuildConfiguration()))) { + if (BuildStepList *buildSteps = m_buildConfig->buildSteps()) { + if ((m_qmakeStep = buildSteps->firstOfType())) { + m_featureFile.setProjectDirectory(m_buildConfig->project()->projectDirectory()); + m_featureFile.read(); + setEnabled(true); + } + } + } + } +} + +QString configAssignment() +{ + static const QString assignment = QString("CONFIG+=") + Constants::PROFILE_NAME; + return assignment; +} + +static const char pathAssignmentPrefix[] = "COCOPATH="; +static const char featuresVar[] = "QMAKEFEATURES"; + +const QStringList CocoQMakeSettings::userArgumentList() const +{ + if (!enabled()) + return {}; + + Utils::ProcessArgs::ConstArgIterator it{m_qmakeStep->userArguments.unexpandedArguments()}; + QStringList result; + + while (it.next()) { + if (it.isSimple()) + result << it.value(); + } + + return result; +} + +Utils::Environment CocoQMakeSettings::buildEnvironment() const +{ + if (!enabled()) + return Utils::Environment(); + + Utils::Environment env = m_buildConfig->environment(); + env.modify(m_buildConfig->userEnvironmentChanges()); + return env; +} + +void CocoQMakeSettings::setQMakeFeatures() const +{ + if (!enabled()) + return; + + Utils::Environment env = buildEnvironment(); + + const QString projectDir = m_buildConfig->project()->projectDirectory().nativePath(); + if (env.value(featuresVar) != projectDir) { + // Bug in prependOrSet(): It does not recognize if QMAKEFEATURES contains a single path + // without a colon and then appends it twice. + env.prependOrSet(featuresVar, projectDir); + } + + Utils::EnvironmentItems diff = m_buildConfig->baseEnvironment().diff(env); + m_buildConfig->setUserEnvironmentChanges(diff); +} + +bool CocoQMakeSettings::environmentSet() const +{ + if (!enabled()) + return true; + + const Utils::Environment env = buildEnvironment(); + const Utils::FilePath projectDir = m_buildConfig->project()->projectDirectory(); + const QString nativeProjectDir = projectDir.nativePath(); + return env.value(featuresVar) == nativeProjectDir + || env.value(featuresVar).startsWith(nativeProjectDir + projectDir.pathListSeparator()); +} + +bool CocoQMakeSettings::validSettings() const +{ + const bool configured = userArgumentList().contains(configAssignment()); + return enabled() && configured && environmentSet() && m_featureFile.exists() + && cocoPathValid(); +} + +void CocoQMakeSettings::setCoverage(bool on) +{ + QString args = m_qmakeStep->userArguments.unexpandedArguments(); + Utils::ProcessArgs::ArgIterator it{&args}; + + while (it.next()) { + if (it.isSimple()) { + const QString value = it.value(); + if (value.startsWith(pathAssignmentPrefix) || value == configAssignment()) + it.deleteArg(); + } + } + if (on) { + it.appendArg(configAssignment()); + it.appendArg(pathAssignment()); + + setQMakeFeatures(); + m_featureFile.write(); + } + + m_qmakeStep->userArguments.setArguments(args); +} + +QString CocoQMakeSettings::saveButtonText() const +{ + return Tr::tr("Save"); +} + +QString CocoQMakeSettings::configChanges() const +{ + return "" + + tableRow( + "Additional qmake arguments: ", + maybeQuote(configAssignment()) + " " + maybeQuote(pathAssignment())) + + tableRow( + "Build environment: ", maybeQuote(QString(featuresVar) + "=" + projectDirectory())) + + tableRow("Feature File: ", maybeQuote(featureFilePath())) + "
"; +} + +QString CocoQMakeSettings::projectDirectory() const +{ + if (enabled()) + return m_buildConfig->project()->projectDirectory().nativePath(); + else + return ""; +} + +void CocoQMakeSettings::write(const QString &options, const QString &tweaks) +{ + m_featureFile.setOptions(options); + m_featureFile.setTweaks(tweaks); + m_featureFile.write(); +} + +QString CocoQMakeSettings::pathAssignment() const +{ + return pathAssignmentPrefix + m_coco.directory().toUserOutput(); +} + +bool CocoQMakeSettings::cocoPathValid() const +{ + Utils::ProcessArgs::ConstArgIterator it{m_qmakeStep->userArguments.unexpandedArguments()}; + + while (it.next()) { + if (it.isSimple()) { + const QString value = it.value(); + if (value.startsWith(pathAssignmentPrefix) && value != pathAssignment()) + return false; + } + } + return true; +} + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/cocoqmakesettings.h b/src/plugins/coco/cocobuild/cocoqmakesettings.h new file mode 100644 index 00000000000..a081bb1e080 --- /dev/null +++ b/src/plugins/coco/cocobuild/cocoqmakesettings.h @@ -0,0 +1,56 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "buildsettings.h" +#include "qmakefeaturefile.h" +#include "settings/cocoinstallation.h" + +#include +#include + +#include +#include + +namespace QmakeProjectManager { +class QMakeStep; +class QmakeBuildConfiguration; +} + +namespace Coco::Internal { + +class CocoProjectWidget; + +class CocoQMakeSettings : public BuildSettings +{ + Q_OBJECT +public: + explicit CocoQMakeSettings(ProjectExplorer::Project *project); + ~CocoQMakeSettings() override; + + void read() override; + bool validSettings() const override; + void setCoverage(bool on) override; + + QString saveButtonText() const override; + QString configChanges() const override; + QString projectDirectory() const override; + void write(const QString &options, const QString &tweaks) override; + +private: + bool environmentSet() const; + QString pathAssignment() const; + const QStringList userArgumentList() const; + Utils::Environment buildEnvironment() const; + void setQMakeFeatures() const; + bool cocoPathValid() const; + + QmakeProjectManager::QmakeBuildConfiguration *m_buildConfig; + QmakeProjectManager::QMakeStep *m_qmakeStep; + + QMakeFeatureFile m_featureFile; + CocoInstallation m_coco; +}; + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/modificationfile.cpp b/src/plugins/coco/cocobuild/modificationfile.cpp new file mode 100644 index 00000000000..a8184021847 --- /dev/null +++ b/src/plugins/coco/cocobuild/modificationfile.cpp @@ -0,0 +1,73 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "modificationfile.h" + +namespace Coco::Internal { + +static void cutTail(QStringList &list) +{ + while (!list.isEmpty() && list.last().trimmed().isEmpty()) + list.removeLast(); +} + +ModificationFile::ModificationFile() {} + +bool ModificationFile::exists() const +{ + return m_filePath.exists(); +} + +void ModificationFile::clear() +{ + m_options.clear(); + m_tweaks.clear(); +} + +QStringList ModificationFile::contentOf(const Utils::FilePath &filePath) const +{ + QFile resource(filePath.nativePath()); + resource.open(QIODevice::ReadOnly | QIODevice::Text); + QTextStream inStream(&resource); + + QStringList result; + QString line; + while (inStream.readLineInto(&line)) + result << line + '\n'; + return result; +} + +QStringList ModificationFile::currentModificationFile() const +{ + QStringList lines; + if (m_filePath.exists()) + lines = contentOf(m_filePath); + else + lines = defaultModificationFile(); + + return lines; +} + +void ModificationFile::setOptions(const QString &options) +{ + m_options = options.split('\n', Qt::SkipEmptyParts); +} + +void ModificationFile::setOptions(const QStringList &options) +{ + m_options = options; +} + +void ModificationFile::setTweaks(const QString &tweaks) +{ + m_tweaks = tweaks.split('\n', Qt::KeepEmptyParts); + cutTail(m_tweaks); +} + +void ModificationFile::setTweaks(const QStringList &tweaks) +{ + m_tweaks = tweaks; + cutTail(m_tweaks); +} + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/modificationfile.h b/src/plugins/coco/cocobuild/modificationfile.h new file mode 100644 index 00000000000..0f156ab2495 --- /dev/null +++ b/src/plugins/coco/cocobuild/modificationfile.h @@ -0,0 +1,51 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +#include + +namespace Coco::Internal { + +class ModificationFile +{ +public: + ModificationFile(); + + virtual void read() = 0; + virtual void write() const = 0; + + virtual void setProjectDirectory(const Utils::FilePath &projectDirectory) = 0; + + virtual QString fileName() const = 0; + QString nativePath() const { return m_filePath.nativePath(); } + bool exists() const; + + const QStringList &options() const { return m_options; } + void setOptions(const QString &options); + + const QStringList &tweaks() const { return m_tweaks; } + void setTweaks(const QString &tweaks); + +protected: + void clear(); + + virtual QStringList defaultModificationFile() const = 0; + QStringList contentOf(const Utils::FilePath &filePath) const; + QStringList currentModificationFile() const; + + void setFilePath(const Utils::FilePath &path) { m_filePath = path; } + + void setOptions(const QStringList &options); + void setTweaks(const QStringList &tweaks); + +private: + QStringList m_options; + QStringList m_tweaks; + + Utils::FilePath m_filePath; +}; + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/qmakefeaturefile.cpp b/src/plugins/coco/cocobuild/qmakefeaturefile.cpp new file mode 100644 index 00000000000..897cbaefa7b --- /dev/null +++ b/src/plugins/coco/cocobuild/qmakefeaturefile.cpp @@ -0,0 +1,105 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qmakefeaturefile.h" + +#include "cocopluginconstants.h" + +#include +#include +#include + +namespace Coco::Internal { + +static const char assignment[] = "COVERAGE_OPTIONS = \\\n"; +static const char tweaksLine[] = "# User-supplied settings follow here:\n"; + +static void cutTail(QStringList &list) +{ + while (!list.isEmpty() && list.last().trimmed().isEmpty()) + list.removeLast(); +} + +QMakeFeatureFile::QMakeFeatureFile() {} + +QString QMakeFeatureFile::fileName() const +{ + return QString(Constants::PROFILE_NAME) + ".prf"; +} + +void QMakeFeatureFile::setProjectDirectory(const Utils::FilePath &projectDirectory) +{ + setFilePath(projectDirectory.pathAppended(fileName())); +} + +QString QMakeFeatureFile::fromFileLine(const QString &line) const +{ + return line.chopped(2).trimmed().replace("\\\"", "\""); +} + +QString QMakeFeatureFile::toFileLine(const QString &option) const +{ + QString line = option.trimmed().replace("\"", "\\\""); + return " " + line + " \\\n"; +} + +void QMakeFeatureFile::read() +{ + clear(); + QStringList file = currentModificationFile(); + + { + QStringList options; + int i = file.indexOf(assignment); + if (i != -1) { + i++; + while (i < file.size() && file[i].endsWith("\\\n")) { + options += fromFileLine(file[i]); + i++; + } + } + setOptions(options); + } + { + QStringList tweaks; + int i = file.indexOf(tweaksLine); + if (i != -1) { + i++; + while (i < file.size()) { + tweaks += file[i].chopped(1); + i++; + } + } + setTweaks(tweaks); + } +} + +void QMakeFeatureFile::write() const +{ + QFile out(nativePath()); + out.open(QIODevice::WriteOnly | QIODevice::Text); + + QTextStream outStream(&out); + for (QString &line : defaultModificationFile()) { + outStream << line; + + if (line.startsWith(assignment)) { + for (const QString &option : options()) { + QString line = toFileLine(option); + if (!line.isEmpty()) + outStream << line; + } + } + } + for (const QString &line : tweaks()) + outStream << line << "\n"; + + out.close(); +} + +QStringList QMakeFeatureFile::defaultModificationFile() const +{ + return contentOf(":/cocoplugin/files/cocoplugin.prf"); +} + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocobuild/qmakefeaturefile.h b/src/plugins/coco/cocobuild/qmakefeaturefile.h new file mode 100644 index 00000000000..3c865a1a25d --- /dev/null +++ b/src/plugins/coco/cocobuild/qmakefeaturefile.h @@ -0,0 +1,34 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "modificationfile.h" + +#include + +#include +#include + +namespace Coco::Internal { + +class QMakeFeatureFile : public ModificationFile +{ +public: + QMakeFeatureFile(); + + void setProjectDirectory(const Utils::FilePath &projectDirectory) override; + void read() override; + void write() const override; + + QString fileName() const override; + +protected: + QStringList defaultModificationFile() const override; + +private: + QString fromFileLine(const QString &line) const; + QString toFileLine(const QString &option) const; +}; + +} // namespace Coco::Internal diff --git a/src/plugins/coco/cocoplugin.cpp b/src/plugins/coco/cocoplugin.cpp index 89047b84249..13b0d690e90 100644 --- a/src/plugins/coco/cocoplugin.cpp +++ b/src/plugins/coco/cocoplugin.cpp @@ -1,17 +1,25 @@ -// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#include "cocobuild/cocobuildstep.h" #include "cocolanguageclient.h" +#include "cocopluginconstants.h" #include "cocotr.h" +#include "settings/cocoprojectsettingswidget.h" +#include "settings/globalsettings.h" +#include "settings/globalsettingspage.h" #include #include #include +#include +#include +#include +#include #include #include - #include #include #include @@ -19,12 +27,17 @@ #include #include #include +#include +#include using namespace Core; using namespace Utils; namespace Coco { +using namespace ProjectExplorer; +using namespace Internal; + class CocoPlugin final : public ExtensionSystem::IPlugin { Q_OBJECT @@ -36,7 +49,7 @@ public: // FIXME: Kill m_client? } - void initialize() final + void initLanguageServer() { ActionBuilder(this, "Coco.startCoco") .setText("Squish Coco ...") @@ -50,53 +63,112 @@ public: m_client->shutdown(); m_client = nullptr; - QDialog dialog(ICore::dialogParent()); - dialog.setModal(true); - auto layout = new QFormLayout(); + CocoInstallation coco; + if (coco.isValid()) { + QDialog dialog(ICore::dialogParent()); + dialog.setModal(true); + auto layout = new QFormLayout(); - const Environment env = Environment::systemEnvironment(); - const FilePath squishCocoPath = FilePath::fromUserInput(env.value("SQUISHCOCO")); - const FilePath candidate = FilePath("coveragebrowser").searchInPath({squishCocoPath}, - FilePath::PrependToPath); + PathChooser csmesChoser; + csmesChoser.setHistoryCompleter("Coco.CSMes.history", true); + csmesChoser.setExpectedKind(PathChooser::File); + csmesChoser.setInitialBrowsePathBackup(PathChooser::homePath()); + csmesChoser.setPromptDialogFilter(Tr::tr("Coco instrumentation files (*.csmes)")); + csmesChoser.setPromptDialogTitle(Tr::tr("Select a Squish Coco Instrumentation File")); + layout->addRow(Tr::tr("CSMes file:"), &csmesChoser); + QDialogButtonBox buttons(QDialogButtonBox::Cancel | QDialogButtonBox::Open); + layout->addWidget(&buttons); + dialog.setLayout(layout); + dialog.resize(480, dialog.height()); - PathChooser cocoChooser; - if (!candidate.isEmpty()) - cocoChooser.setFilePath(candidate); - cocoChooser.setExpectedKind(PathChooser::Command); - cocoChooser.setPromptDialogTitle(Tr::tr("Select a Squish Coco CoverageBrowser Executable")); + QObject::connect(&buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + QObject::connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - cocoChooser.setHistoryCompleter("Coco.CoverageBrowser.history", true); - layout->addRow(Tr::tr("CoverageBrowser:"), &cocoChooser); - PathChooser csmesChoser; - csmesChoser.setHistoryCompleter("Coco.CSMes.history", true); - csmesChoser.setExpectedKind(PathChooser::File); - csmesChoser.setInitialBrowsePathBackup(FileUtils::homePath()); - csmesChoser.setPromptDialogFilter(Tr::tr("Coco instrumentation files (*.csmes)")); - csmesChoser.setPromptDialogTitle(Tr::tr("Select a Squish Coco Instrumentation File")); - layout->addRow(Tr::tr("CSMes:"), &csmesChoser); - QDialogButtonBox buttons(QDialogButtonBox::Cancel | QDialogButtonBox::Open); - layout->addItem(new QSpacerItem(0, 20, QSizePolicy::Expanding, QSizePolicy::MinimumExpanding)); - layout->addWidget(&buttons); - dialog.setLayout(layout); - dialog.resize(480, dialog.height()); - - QObject::connect(&buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); - QObject::connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - - if (dialog.exec() == QDialog::Accepted) { - const FilePath cocoPath = cocoChooser.filePath(); - const FilePath csmesPath = csmesChoser.filePath(); - if (cocoPath.isExecutableFile() && csmesPath.exists()) { - m_client = new CocoLanguageClient(cocoPath, csmesPath); - m_client->start(); + if (dialog.exec() == QDialog::Accepted) { + const FilePath cocoPath = coco.coverageBrowserPath(); + const FilePath csmesPath = csmesChoser.filePath(); + if (cocoPath.isExecutableFile() && csmesPath.exists()) { + m_client = new CocoLanguageClient(cocoPath, csmesPath); + m_client->start(); + } } + } else { + QMessageBox msg; + msg.setText(Tr::tr("No valid CoverageScanner found.")); + QPushButton *configButton = msg.addButton(Tr::tr("Configure"), QMessageBox::AcceptRole); + msg.setStandardButtons(QMessageBox::Cancel); + msg.exec(); + + if (msg.clickedButton() == configButton) + Core::ICore::showOptionsDialog(Constants::COCO_SETTINGS_PAGE_ID); } } + bool initialize(const QStringList &arguments, QString *errorString); + void addEntryToProjectSettings(); + private: + static void addBuildStep(ProjectExplorer::Target *target); + + QMakeStepFactory m_qmakeStepFactory; + CMakeStepFactory m_cmakeStepFactory; + CocoLanguageClient *m_client = nullptr; }; +void CocoPlugin::addBuildStep(Target *target) +{ + for (BuildConfiguration *config : target->buildConfigurations()) { + if (BuildSettings::supportsBuildConfig(*config)) { + BuildStepList *steps = config->buildSteps(); + + if (!steps->contains(Constants::COCO_STEP_ID)) + steps->insertStep(0, CocoBuildStep::create(config)); + + steps->firstOfType()->display(config); + } + } +} + +bool CocoPlugin::initialize(const QStringList &arguments, QString *errorString) +{ + Q_UNUSED(arguments) + Q_UNUSED(errorString) + + GlobalSettings::read(); + GlobalSettingsPage::instance().widget(); + addEntryToProjectSettings(); + + connect(ProjectManager::instance(), &ProjectManager::projectAdded, this, [&](Project *project) { + if (Target *target = project->activeTarget()) + addBuildStep(target); + + connect(project, &Project::addedTarget, this, [](Target *target) { + addBuildStep(target); + }); + }); + + initLanguageServer(); + + return true; +} + +void CocoPlugin::addEntryToProjectSettings() +{ + auto panelFactory = new ProjectPanelFactory; + panelFactory->setPriority(50); + panelFactory->setDisplayName(tr("Coco Code Coverage")); + panelFactory->setSupportsFunction([](Project *project) { + if (Target *target = project->activeTarget()) { + if (BuildConfiguration *abc = target->activeBuildConfiguration()) + return BuildSettings::supportsBuildConfig(*abc); + } + return false; + }); + panelFactory->setCreateWidgetFunction( + [](Project *project) { return new CocoProjectSettingsWidget(project); }); +} + } // namespace Coco #include "cocoplugin.moc" diff --git a/src/plugins/coco/cocoplugin.qrc b/src/plugins/coco/cocoplugin.qrc new file mode 100644 index 00000000000..bb52bda91b1 --- /dev/null +++ b/src/plugins/coco/cocoplugin.qrc @@ -0,0 +1,10 @@ + + + images/SquishCoco_48x48.png + files/cocoplugin.prf + files/cocoplugin.cmake + files/cocoplugin-clang.cmake + files/cocoplugin-gcc.cmake + files/cocoplugin-visualstudio.cmake + + diff --git a/src/plugins/coco/cocoplugin_global.h b/src/plugins/coco/cocoplugin_global.h new file mode 100644 index 00000000000..b89de4ccaea --- /dev/null +++ b/src/plugins/coco/cocoplugin_global.h @@ -0,0 +1,12 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +#if defined(COCO_LIBRARY) +# define COCOPLUGINSHARED_EXPORT Q_DECL_EXPORT +#else +# define COCOPLUGINSHARED_EXPORT Q_DECL_IMPORT +#endif diff --git a/src/plugins/coco/cocopluginconstants.h b/src/plugins/coco/cocopluginconstants.h new file mode 100644 index 00000000000..8fd36ee3bce --- /dev/null +++ b/src/plugins/coco/cocopluginconstants.h @@ -0,0 +1,23 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +namespace Coco { +namespace Constants { + +const char ACTION_ID[] = "coco.Action"; +const char MENU_ID[] = "coco.Menu"; +const char COCO_STEP_ID[] = "Cocoplugin.BuildStep"; + +const char PROFILE_NAME[] = "cocoplugin"; // Name of the Coco profile file + +const char COCO_SETTINGS_GROUP[] = "Coco"; +const char COCO_SETTINGS_PAGE_ID[] = "A.CocoOptions"; + +// Project settings +const char SETTINGS_NAME_KEY[] = "CocoProjectSettings"; +const char SELECTION_DIR_KEY[] = "SelectionDir"; + +} // namespace Constants +} // namespace Coco diff --git a/src/plugins/coco/common.cpp b/src/plugins/coco/common.cpp new file mode 100644 index 00000000000..e822e6eb7b7 --- /dev/null +++ b/src/plugins/coco/common.cpp @@ -0,0 +1,30 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "common.h" + +#include "cocopluginconstants.h" + +#include + +QString maybeQuote(const QString &str) +{ + if ((str.contains(' ') || str.contains('\t')) && !str.startsWith('"')) + return '"' + str + '"'; + else + return str; +} + +void logSilently(const QString &msg) +{ + static const QString prefix = QString{"[%1] "}.arg(Coco::Constants::PROFILE_NAME); + + Core::MessageManager::writeSilently(prefix + msg); +} + +void logFlashing(const QString &msg) +{ + static const QString prefix = QString{"[%1] "}.arg(Coco::Constants::PROFILE_NAME); + + Core::MessageManager::writeFlashing(prefix + msg); +} diff --git a/src/plugins/coco/common.h b/src/plugins/coco/common.h new file mode 100644 index 00000000000..fde75dc617e --- /dev/null +++ b/src/plugins/coco/common.h @@ -0,0 +1,14 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef COMMON_H +#define COMMON_H + +#include + +QString maybeQuote(const QString &str); + +void logSilently(const QString &msg); +void logFlashing(const QString &msg); + +#endif // COMMON_H diff --git a/src/plugins/coco/files/cocoplugin-clang.cmake b/src/plugins/coco/files/cocoplugin-clang.cmake new file mode 100644 index 00000000000..697b87465e0 --- /dev/null +++ b/src/plugins/coco/files/cocoplugin-clang.cmake @@ -0,0 +1,8 @@ +# Created by the Coco Qt Creator plugin. Do not edit! + +set(CMAKE_C_COMPILER clang) +set(CMAKE_CXX_COMPILER clang++) +set(CMAKE_AR ar) +set(CMAKE_LINKER clang) + +include(cocoplugin.cmake) diff --git a/src/plugins/coco/files/cocoplugin-gcc.cmake b/src/plugins/coco/files/cocoplugin-gcc.cmake new file mode 100644 index 00000000000..1271c3115a3 --- /dev/null +++ b/src/plugins/coco/files/cocoplugin-gcc.cmake @@ -0,0 +1,8 @@ +# Created by the Coco Qt Creator plugin. Do not edit! + +set(CMAKE_C_COMPILER gcc) +set(CMAKE_CXX_COMPILER g++) +set(CMAKE_AR ar) +set(CMAKE_LINKER gcc) + +include(cocoplugin.cmake) diff --git a/src/plugins/coco/files/cocoplugin-visualstudio.cmake b/src/plugins/coco/files/cocoplugin-visualstudio.cmake new file mode 100644 index 00000000000..d72f74db882 --- /dev/null +++ b/src/plugins/coco/files/cocoplugin-visualstudio.cmake @@ -0,0 +1,8 @@ +# Created by the Coco Qt Creator plugin. Do not edit! + +set(CMAKE_C_COMPILER cl) +set(CMAKE_CXX_COMPILER cl) +set(CMAKE_AR lib) +set(CMAKE_LINKER link) + +include(cocoplugin.cmake) diff --git a/src/plugins/coco/files/cocoplugin.cmake b/src/plugins/coco/files/cocoplugin.cmake new file mode 100644 index 00000000000..78ea51de860 --- /dev/null +++ b/src/plugins/coco/files/cocoplugin.cmake @@ -0,0 +1,87 @@ +# Created by the Coco Qt Creator plugin. Do not edit! + +set(coverage_flags_list +) +list(JOIN coverage_flags_list " " coverage_flags) + +foreach(var IN ITEMS CMAKE_C_COMPILER CMAKE_CXX_COMPILER) + if(NOT DEFINED ${var}) + message(FATAL_ERROR "Variable ${var} must be defined.") + endif() +endforeach() + +set(CMAKE_C_FLAGS_INIT "${coverage_flags}" + CACHE STRING "Coverage flags for the C compiler." FORCE) +set(CMAKE_CXX_FLAGS_INIT "${coverage_flags}" + CACHE STRING "Coverage flags for the C++ compiler." FORCE) +set(CMAKE_EXE_LINKER_FLAGS_INIT "${coverage_flags}" + CACHE STRING "Coverage flags for the linker." FORCE) +set(CMAKE_SHARED_LINKER_FLAGS_INIT "${coverage_flags}" + CACHE STRING "Coverage flags to link shared libraries." FORCE) +set(CMAKE_STATIC_LINKER_FLAGS_INIT "${coverage_flags}" + CACHE STRING "Coverage flags to link static libraries." FORCE) + +if (DEFINED ENV{SQUISHCOCO}) + set(cocopath $ENV{SQUISHCOCO}) +else() + find_file(cocopath SquishCoco + PATHS "$ENV{HOME}" /opt/ "/Applications" + REQUIRED + NO_DEFAULT_PATH + ) +endif() + +if(CMAKE_HOST_APPLE) + set(wrapperdir "${cocopath}/") +elseif(CMAKE_HOST_UNIX) + set(wrapperdir "${cocopath}/bin/") +elseif(MINGW) + set(wrapperdir "${cocopath}\\bin\\") +else() + set(wrapperdir "${cocopath}\\" ) +endif() + +get_filename_component(c_compiler ${CMAKE_C_COMPILER} NAME) +find_program(code_coverage_c_compiler cs${c_compiler} + PATHS ${wrapperdir} + REQUIRED NO_DEFAULT_PATH) +set(CMAKE_C_COMPILER "${code_coverage_c_compiler}" + CACHE FILEPATH "CoverageScanner wrapper for C compiler" FORCE) + +get_filename_component(cxx_compiler ${CMAKE_CXX_COMPILER} NAME) +find_program(code_coverage_cxx_compiler cs${cxx_compiler} + PATHS ${wrapperdir} + REQUIRED NO_DEFAULT_PATH) +set(CMAKE_CXX_COMPILER "${code_coverage_cxx_compiler}" + CACHE FILEPATH "CoverageScanner wrapper for C++ compiler" FORCE) + +if(DEFINED CMAKE_LINKER) + get_filename_component(linker_prog ${CMAKE_LINKER} NAME) + find_program(code_coverage_linker cs${linker_prog} + PATHS ${wrapperdir} + REQUIRED NO_DEFAULT_PATH) + set(CMAKE_LINKER "${code_coverage_linker}" + CACHE FILEPATH "CoverageScanner wrapper for linker" FORCE) +elseif(${c_compiler} STREQUAL "cl.exe") # special case for Visual Studio + find_program(code_coverage_linker "cslink.exe" + PATHS ${wrapperdir} + REQUIRED NO_DEFAULT_PATH) + set(CMAKE_LINKER "${code_coverage_linker}" + CACHE FILEPATH "CoverageScanner wrapper for linker" FORCE) +endif() + +if(DEFINED CMAKE_AR) + get_filename_component(ar_prog ${CMAKE_AR} NAME) + find_program(code_coverage_ar cs${ar_prog} + PATHS ${wrapperdir} + REQUIRED NO_DEFAULT_PATH) + set(CMAKE_AR "${code_coverage_ar}" + CACHE FILEPATH "CoverageScanner wrapper for ar" FORCE) +endif() + +mark_as_advanced( + cocopath + code_coverage_c_compiler code_coverage_cxx_compiler code_coverage_linker code_coverage_ar +) + +# User-supplied settings follow here: diff --git a/src/plugins/coco/files/cocoplugin.prf b/src/plugins/coco/files/cocoplugin.prf new file mode 100644 index 00000000000..b68c2c671da --- /dev/null +++ b/src/plugins/coco/files/cocoplugin.prf @@ -0,0 +1,33 @@ +# Created by the Coco plugin. Do not edit! + +COVERAGE_OPTIONS = \ + +defineReplace(toCoco) { + cmd = $$1 + path = $$take_first(cmd) + prog = $$basename(path) + + return(cs$$prog $$cmd) +} + +isEmpty(COCOPATH): error(The variable COCOPATH must be set) + +macos: wrapperdir = $$COCOPATH +else: unix: wrapperdir = $$COCOPATH/bin +else: win32: { + win32-arm-msvc*|win32-x86-msvc*: wrapperdir = $$COCOPATH/visualstudio + else: win32-arm64-msvc*|win32-x64-msvc*: wrapperdir = $$COCOPATH/visualstudio_x64 + else: wrapperdir = $$COCOPATH +} + +QMAKE_CFLAGS += $$COVERAGE_OPTIONS +QMAKE_CXXFLAGS += $$COVERAGE_OPTIONS +QMAKE_LFLAGS += $$COVERAGE_OPTIONS + +QMAKE_AR = $$wrapperdir/$$toCoco($$QMAKE_AR) +QMAKE_CC = $$wrapperdir/$$toCoco($$QMAKE_CC) +QMAKE_CXX = $$wrapperdir/$$toCoco($$QMAKE_CXX) +QMAKE_LINK = $$wrapperdir/$$toCoco($$QMAKE_LINK) +QMAKE_LINK_SHLIB_CMD = $$wrapperdir/$$toCoco($$QMAKE_LINK_SHLIB_CMD) + +# User-supplied settings follow here: diff --git a/src/plugins/coco/images/SquishCoco_48x48.png b/src/plugins/coco/images/SquishCoco_48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..74a68a1727c9257c5d62005b940d91b5bf679303 GIT binary patch literal 594 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F$mbIO@o>nPIdPh>vfk#|v0@cRXRLX8`1dvQ{l3@jdw(%<6YGK7 z-<4-d-;QIAWAf+JcR!H!`chovJi9$i6Y_o-8F=nE%*?~=Eq>_dDG@`4mv)uTxqA5$ ze>djjUC}T)FMRUu4VS(WEkj4;pKe0`HdanZiD5e9dDZdXq=hTY6&}6S%2f8Q%h@(P zQSQm%?XB*wbEPF-NI$837W$yEN$r*Lm47T+X^p3LJLzx!p;W?GGV7=PMZOho)&eZI zubO6kowR=I!h!`!Vm0OqWCMIPWPZrj^1JZ9Z3y3Uu0nka^Qwz{EUs(gl6LQURynuK z!Sw%j=A9d7I+)#Dx$bve?7Jh2`Fhgh*f*V=_w(g^Q;-S%GB4V0Wp>W}TYCO=)guRU zj+XUoTbNgM{^5MV7NX4PUdmzrhf7y*hS%gDPyQ@tZ8(zXTqt(l!r|u%&W_@nyS9X% ztBY{Gx^UM~M!RX!b87M#zlk#*5ZQQVMWD}eA>nu89|DT5ZMk8ax9j1`WXsCJ_*%y~ u=_g$i`gZSkI^@MX=X`|D#?smXZuL#83pf6dw_;#mVDNPHb6Mw<&;$U=ru8cT literal 0 HcmV?d00001 diff --git a/src/plugins/coco/settings/cocoinstallation.cpp b/src/plugins/coco/settings/cocoinstallation.cpp new file mode 100644 index 00000000000..77d466eed3e --- /dev/null +++ b/src/plugins/coco/settings/cocoinstallation.cpp @@ -0,0 +1,177 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "cocoinstallation.h" + +#include "cocotr.h" +#include "common.h" +#include "globalsettings.h" + +#include +#include + +#include +#include +#include + +namespace Coco::Internal { + +struct CocoInstallationPrivate +{ + Utils::FilePath cocoPath; + bool isValid = false; + QString errorMessage = Tr::tr("Error: Coco installation directory not set. (This can't happen.)"); +}; + +CocoInstallationPrivate *CocoInstallation::d = nullptr; + +CocoInstallation::CocoInstallation() +{ + if (!d) + d = new CocoInstallationPrivate; +} + +Utils::FilePath CocoInstallation::directory() const +{ + return d->cocoPath; +} + +Utils::FilePath CocoInstallation::coverageBrowserPath() const +{ + QString browserPath; + + if (Utils::HostOsInfo::isAnyUnixHost() || Utils::HostOsInfo::isMacHost()) + browserPath = "bin/coveragebrowser"; + else + browserPath = "coveragebrowser.exe"; + + return d->cocoPath.resolvePath(browserPath); +} + +void CocoInstallation::setDirectory(const Utils::FilePath &dir) +{ + if (isCocoDirectory(dir)) { + d->cocoPath = dir; + d->isValid = true; + d->errorMessage = ""; + verifyCocoDirectory(); + } + else { + d->cocoPath = Utils::FilePath(); + d->isValid = false; + d->errorMessage + = Tr::tr("Error: Coco installation directory not found at \"%1\".").arg(dir.nativePath()); + } +} + +Utils::FilePath CocoInstallation::coverageScannerPath(const Utils::FilePath &cocoDir) const +{ + QString scannerPath; + + if (Utils::HostOsInfo::isAnyUnixHost() || Utils::HostOsInfo::isMacHost()) + scannerPath = "bin/coveragescanner"; + else + scannerPath = "coveragescanner.exe"; + + return cocoDir.resolvePath(scannerPath); +} + +bool CocoInstallation::isCocoDirectory(const Utils::FilePath &cocoDir) const +{ + return coverageScannerPath(cocoDir).exists(); +} + +void CocoInstallation::logError(const QString &msg) +{ + logFlashing(msg); + d->isValid = false; + d->errorMessage = msg; +} + +bool CocoInstallation::verifyCocoDirectory() +{ + QString coveragescanner = coverageScannerPath(d->cocoPath).nativePath(); + + QProcess proc; + proc.setProgram(coveragescanner); + proc.setArguments({"--cs-help"}); + proc.start(); + + if (!proc.waitForStarted()) { + logError(Tr::tr("Error: Coveragescanner at \"%1\" did not start.").arg(coveragescanner)); + return false; + } + + if (!proc.waitForFinished()) { + logError(Tr::tr("Error: Coveragescanner at \"%1\" did not finish.").arg(coveragescanner)); + return false; + } + + QString result = QString::fromLatin1(proc.readAll()); + static const QRegularExpression linebreak("\n|\r\n|\r"); + QStringList lines = result.split(linebreak, Qt::SkipEmptyParts); + + const qsizetype n = lines.size(); + if (n >= 2 && lines[n - 2].startsWith("Version:") && lines[n - 1].startsWith("Date:")) { + logSilently(Tr::tr("Valid CoverageScanner found at \"%1\":").arg(coveragescanner)); + logSilently(" " + lines[n - 2]); + logSilently(" " + lines[n - 1]); + return true; + } else { + logError( + Tr::tr("Error: Coveragescanner at \"%1\" did not run correctly.").arg(coveragescanner)); + for (const QString &l : lines) { + logSilently(l); + } + return false; + } +} + +bool CocoInstallation::isValid() const +{ + return d->isValid; +} + +QString CocoInstallation::errorMessage() const +{ + return d->errorMessage; +} + +void CocoInstallation::tryPath(const QString &path) +{ + if (d->isValid) + return; + + const auto fpath = Utils::FilePath::fromString(path); + const QString nativePath = fpath.nativePath(); + if (isCocoDirectory(fpath)) { + logSilently(Tr::tr("Found Coco directory \"%1\".").arg(nativePath)); + setDirectory(fpath); + GlobalSettings::save(); + } else + logSilently(Tr::tr("Checked Coco directory \"%1\".").arg(nativePath)); +} + +QString CocoInstallation::envVar(const QString &var) const +{ + return QProcessEnvironment::systemEnvironment().value(var); +} + +void CocoInstallation::findDefaultDirectory() +{ + if (Utils::HostOsInfo::isMacHost()) + tryPath("/Applications/SquishCoco"); + else if (Utils::HostOsInfo::isAnyUnixHost()) { + tryPath((Utils::FileUtils::homePath() / "SquishCoco").nativePath()); + tryPath("/opt/SquishCoco"); + } else { + tryPath(envVar("SQUISHCOCO")); + QStringList homeDirs = QStandardPaths::standardLocations(QStandardPaths::HomeLocation); + if (!homeDirs.isEmpty()) + tryPath(homeDirs[0] + "/squishcoco"); + tryPath(envVar("ProgramFiles") + "\\squishcoco"); + tryPath(envVar("ProgramFiles(x86)") + "\\squishcoco"); + } +} + +} // namespace Coco::Internal diff --git a/src/plugins/coco/settings/cocoinstallation.h b/src/plugins/coco/settings/cocoinstallation.h new file mode 100644 index 00000000000..73d83bf704d --- /dev/null +++ b/src/plugins/coco/settings/cocoinstallation.h @@ -0,0 +1,39 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +class QString; + +namespace Coco::Internal { + +struct CocoInstallationPrivate; + +// Borg pattern: There are many instances of this class, but all are the same. +class CocoInstallation +{ +public: + CocoInstallation(); + + Utils::FilePath directory() const; + Utils::FilePath coverageBrowserPath() const; + void setDirectory(const Utils::FilePath &dir); + void findDefaultDirectory(); + + bool isValid() const; + QString errorMessage() const; + +private: + Utils::FilePath coverageScannerPath(const Utils::FilePath &cocoDir) const; + void logError(const QString &msg); + bool isCocoDirectory(const Utils::FilePath &cocoDir) const; + bool verifyCocoDirectory(); + void tryPath(const QString &path); + QString envVar(const QString &var) const; + + static CocoInstallationPrivate *d; +}; + +} // namespace Coco::Internal diff --git a/src/plugins/coco/settings/cocoprojectsettingswidget.cpp b/src/plugins/coco/settings/cocoprojectsettingswidget.cpp new file mode 100644 index 00000000000..8e6f10cc5ec --- /dev/null +++ b/src/plugins/coco/settings/cocoprojectsettingswidget.cpp @@ -0,0 +1,40 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "cocoprojectsettingswidget.h" + +#include "cocobuild/cocoprojectwidget.h" +#include "cocopluginconstants.h" + +#include +#include +#include +#include +#include + +#include + +namespace Coco::Internal { + +CocoProjectSettingsWidget::CocoProjectSettingsWidget(ProjectExplorer::Project *project) + : m_layout{new QVBoxLayout} +{ + setUseGlobalSettingsCheckBoxVisible(false); + setGlobalSettingsId(Constants::COCO_SETTINGS_PAGE_ID); + + if (auto *target = project->activeTarget()) { + auto abc = target->activeBuildConfiguration(); + + if (abc->id() == QmakeProjectManager::Constants::QMAKE_BC_ID + || abc->id() == CMakeProjectManager::Constants::CMAKE_BUILDCONFIGURATION_ID) + m_layout->addWidget(new CocoProjectWidget(project, *abc)); + } + setLayout(m_layout); +} + +CocoProjectSettingsWidget::~CocoProjectSettingsWidget() +{ + delete m_layout; +} + +} // namespace Coco::Internal diff --git a/src/plugins/coco/settings/cocoprojectsettingswidget.h b/src/plugins/coco/settings/cocoprojectsettingswidget.h new file mode 100644 index 00000000000..c201a55ece7 --- /dev/null +++ b/src/plugins/coco/settings/cocoprojectsettingswidget.h @@ -0,0 +1,28 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +#include + +namespace ProjectExplorer { +class Project; +} + +namespace Coco::Internal { + +class CocoProjectSettingsWidget : public ProjectExplorer::ProjectSettingsWidget +{ + Q_OBJECT + +public: + explicit CocoProjectSettingsWidget(ProjectExplorer::Project *project); + ~CocoProjectSettingsWidget(); + +private: + QVBoxLayout *m_layout; +}; + +} // namespace Coco::Internal diff --git a/src/plugins/coco/settings/globalsettings.cpp b/src/plugins/coco/settings/globalsettings.cpp new file mode 100644 index 00000000000..594355964ee --- /dev/null +++ b/src/plugins/coco/settings/globalsettings.cpp @@ -0,0 +1,53 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "globalsettings.h" + +#include "cocoinstallation.h" +#include "cocopluginconstants.h" + +#include +#include + +#include +#include + +namespace Coco::Internal { +namespace GlobalSettings { + +static const char DIRECTORY[] = "CocoDirectory"; + +void read() +{ + CocoInstallation coco; + bool directoryInSettings = false; + + Utils::QtcSettings *s = Core::ICore::settings(); + s->beginGroup(Constants::COCO_SETTINGS_GROUP); + const QStringList keys = s->allKeys(); + for (const QString &keyString : keys) { + Utils::Key key(keyString.toLatin1()); + if (key == DIRECTORY) { + coco.setDirectory(Utils::FilePath::fromUserInput(s->value(key).toString())); + directoryInSettings = true; + } else + s->remove(key); + } + s->endGroup(); + + if (!directoryInSettings) + coco.findDefaultDirectory(); + + GlobalSettings::save(); +} + +void save() +{ + Utils::QtcSettings *s = Core::ICore::settings(); + s->beginGroup(Constants::COCO_SETTINGS_GROUP); + s->setValue(DIRECTORY, CocoInstallation().directory().toUserOutput()); + s->endGroup(); +} + +} // namespace GlobalSettings +} // namespace Coco::Internal diff --git a/src/plugins/coco/settings/globalsettings.h b/src/plugins/coco/settings/globalsettings.h new file mode 100644 index 00000000000..e57f7e790f3 --- /dev/null +++ b/src/plugins/coco/settings/globalsettings.h @@ -0,0 +1,13 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +namespace Coco::Internal { +namespace GlobalSettings { + +void read(); +void save(); + +} // namespace GlobalSettings +} // namespace Coco::Internal diff --git a/src/plugins/coco/settings/globalsettingspage.cpp b/src/plugins/coco/settings/globalsettingspage.cpp new file mode 100644 index 00000000000..b4eda76d0ba --- /dev/null +++ b/src/plugins/coco/settings/globalsettingspage.cpp @@ -0,0 +1,121 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "globalsettingspage.h" + +#include "cocoinstallation.h" +#include "cocopluginconstants.h" +#include "cocotr.h" +#include "globalsettings.h" + +#include +#include + +namespace Coco::Internal { + +GlobalSettingsWidget::GlobalSettingsWidget(QFrame *parent) + : QFrame(parent) +{ + m_cocoPathAspect.setDefaultPathValue(m_coco.directory()); + m_cocoPathAspect.setExpectedKind(Utils::PathChooser::ExistingDirectory); + m_cocoPathAspect.setPromptDialogTitle(Tr::tr("Coco Installation Directory")); + + connect( + &m_cocoPathAspect, + &Utils::FilePathAspect::changed, + this, + &GlobalSettingsWidget::onCocoPathChanged); + + using namespace Layouting; + Form{ + Column{ + Row{Tr::tr("Coco Directory"), m_cocoPathAspect}, + Row{m_messageLabel}} + }.attachTo(this); +} + +void GlobalSettingsWidget::onCocoPathChanged() +{ + if (!verifyCocoDirectory(m_cocoPathAspect())) + m_cocoPathAspect.setValue(m_previousCocoDir, Utils::BaseAspect::BeQuiet); +} + +bool GlobalSettingsWidget::verifyCocoDirectory(const Utils::FilePath &cocoDir) +{ + m_coco.setDirectory(cocoDir); + m_messageLabel.setText(m_coco.errorMessage()); + if (m_coco.isValid()) + m_messageLabel.setIconType(Utils::InfoLabel::None); + else + m_messageLabel.setIconType(Utils::InfoLabel::Error); + return m_coco.isValid(); +} + +void GlobalSettingsWidget::apply() +{ + if (!verifyCocoDirectory(widgetCocoDir())) + return; + + m_coco.setDirectory(widgetCocoDir()); + GlobalSettings::save(); + + emit updateCocoDir(); +} + +void GlobalSettingsWidget::cancel() +{ + m_coco.setDirectory(m_previousCocoDir); +} + +void GlobalSettingsWidget::setVisible(bool visible) +{ + QFrame::setVisible(visible); + m_previousCocoDir = m_coco.directory(); +} + +Utils::FilePath GlobalSettingsWidget::widgetCocoDir() const +{ + return Utils::FilePath::fromUserInput(m_cocoPathAspect.value()); +} + +GlobalSettingsPage::GlobalSettingsPage() + : m_widget(nullptr) +{ + setId(Constants::COCO_SETTINGS_PAGE_ID); + setDisplayName(QCoreApplication::translate("Coco", "Coco")); + setCategory("I.Coco"); // Category I contains also the C++ settings. + setDisplayCategory(QCoreApplication::translate("Coco", "Coco")); + setCategoryIconPath(":/cocoplugin/images/SquishCoco_48x48.png"); +} + +GlobalSettingsPage &GlobalSettingsPage::instance() +{ + static GlobalSettingsPage instance; + return instance; +} + +GlobalSettingsWidget *GlobalSettingsPage::widget() +{ + if (!m_widget) + m_widget = new GlobalSettingsWidget; + return m_widget; +} + +void GlobalSettingsPage::apply() +{ + if (m_widget) + m_widget->apply(); +} + +void GlobalSettingsPage::cancel() +{ + if (m_widget) + m_widget->cancel(); +} + +void GlobalSettingsPage::finish() +{ + delete m_widget; +} + +} // namespace Coco::Internal diff --git a/src/plugins/coco/settings/globalsettingspage.h b/src/plugins/coco/settings/globalsettingspage.h new file mode 100644 index 00000000000..e55d6881b86 --- /dev/null +++ b/src/plugins/coco/settings/globalsettingspage.h @@ -0,0 +1,59 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "cocoinstallation.h" + +#include + +#include + +namespace Coco::Internal { + +class GlobalSettingsWidget : public QFrame +{ + Q_OBJECT + +public: + GlobalSettingsWidget(QFrame *parent = nullptr); + + void apply(); + void cancel(); + +signals: + void updateCocoDir(); + +public slots: + void setVisible(bool visible) override; + +private: + void onCocoPathChanged(); + + Utils::FilePath widgetCocoDir() const; + bool verifyCocoDirectory(const Utils::FilePath &cocoDir); + + Utils::FilePathAspect m_cocoPathAspect; + Utils::TextDisplay m_messageLabel; + + CocoInstallation m_coco; + Utils::FilePath m_previousCocoDir; +}; + +class GlobalSettingsPage : public Core::IOptionsPage +{ +public: + static GlobalSettingsPage &instance(); + + GlobalSettingsWidget *widget() override; + void apply() override; + void cancel() override; + void finish() override; + +private: + GlobalSettingsPage(); + + QPointer m_widget; +}; + +} // namespace Coco::Internal