1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-11-21 17:17:06 +02:00

[launcher] build for Android

also embeds icons and translations as Qt resources instead of reading from disk
This commit is contained in:
Andrey Filipenkov 2024-03-03 00:24:00 +03:00
parent cbc418fc62
commit 8cee8b72a6
18 changed files with 345 additions and 212 deletions

View File

@ -58,13 +58,22 @@ option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" O
# Platform-specific options # Platform-specific options
if(ANDROID) if(ANDROID)
set(ANDROID_TARGET_SDK_VERSION "33" CACHE STRING "Android target SDK version")
set(ANDROIDDEPLOYQT_OPTIONS "" CACHE STRING "Additional androiddeployqt options separated by semi-colon")
set(ANDROID_GRADLE_PROPERTIES "" CACHE STRING "Additional Gradle properties separated by semi-colon")
set(ENABLE_STATIC_LIBS ON) set(ENABLE_STATIC_LIBS ON)
set(ENABLE_LAUNCHER OFF) set(ENABLE_LAUNCHER ON)
else() else()
option(ENABLE_STATIC_LIBS "Build library and all components such as AI statically" OFF) option(ENABLE_STATIC_LIBS "Build library and all components such as AI statically" OFF)
option(ENABLE_LAUNCHER "Enable compilation of launcher" ON) option(ENABLE_LAUNCHER "Enable compilation of launcher" ON)
endif() endif()
if(APPLE_IOS)
set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix")
set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen")
endif()
if(APPLE_IOS OR ANDROID) if(APPLE_IOS OR ANDROID)
set(ENABLE_MONOLITHIC_INSTALL OFF) set(ENABLE_MONOLITHIC_INSTALL OFF)
set(ENABLE_SINGLE_APP_BUILD ON) set(ENABLE_SINGLE_APP_BUILD ON)
@ -100,11 +109,6 @@ if (ENABLE_STATIC_LIBS AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif() endif()
if(APPLE_IOS)
set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix")
set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen")
endif()
if(ENABLE_COLORIZED_COMPILER_OUTPUT) if(ENABLE_COLORIZED_COMPILER_OUTPUT)
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
add_compile_options(-fcolor-diagnostics) add_compile_options(-fcolor-diagnostics)
@ -147,10 +151,6 @@ set(CMAKE_MODULE_PATH ${CMAKE_HOME_DIRECTORY}/cmake_modules ${PROJECT_SOURCE_DIR
include(VCMIUtils) include(VCMIUtils)
include(VersionDefinition) include(VersionDefinition)
if(ANDROID)
set(VCMI_VERSION "${APP_SHORT_VERSION}")
configure_file("android/GeneratedVersion.java.in" "${CMAKE_SOURCE_DIR}/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/GeneratedVersion.java" @ONLY)
endif()
vcmi_print_important_variables() vcmi_print_important_variables()
@ -575,8 +575,12 @@ elseif(APPLE)
endif() endif()
elseif(ANDROID) elseif(ANDROID)
include(GNUInstallDirs) include(GNUInstallDirs)
set(LIB_DIR "jniLibs/${ANDROID_ABI}") set(LIB_DIR "libs/${ANDROID_ABI}")
set(DATA_DIR "assets")
# required by Qt
set(androidPackageSourceDir "${CMAKE_SOURCE_DIR}/android")
set(androidQtBuildDir "${CMAKE_BINARY_DIR}/android-build")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${androidQtBuildDir}/${LIB_DIR}")
else() else()
# includes lib path which determines where to install shared libraries (either /lib or /lib64) # includes lib path which determines where to install shared libraries (either /lib or /lib64)
include(GNUInstallDirs) include(GNUInstallDirs)
@ -621,6 +625,13 @@ else()
set(SCRIPTING_LIB_DIR "${LIB_DIR}/scripting") set(SCRIPTING_LIB_DIR "${LIB_DIR}/scripting")
endif() endif()
# common Qt paths
if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
get_target_property(qmakePath Qt${QT_VERSION_MAJOR}::qmake IMPORTED_LOCATION)
get_filename_component(qtDir "${qmakePath}/../../" ABSOLUTE)
set(qtBinDir "${qtDir}/bin")
endif()
####################################### #######################################
# Add subdirectories # # Add subdirectories #
####################################### #######################################
@ -682,32 +693,15 @@ endif()
####################################### #######################################
if(ANDROID) if(ANDROID)
string(REPLACE ";" "\n" ANDROID_GRADLE_PROPERTIES_MULTILINE "${ANDROID_GRADLE_PROPERTIES}")
file(WRITE "${androidPackageSourceDir}/vcmi-app/gradle.properties" "signingRoot=${CMAKE_SOURCE_DIR}/CI/android\n${ANDROID_GRADLE_PROPERTIES_MULTILINE}")
if(ANDROID_STL MATCHES "_shared$") if(ANDROID_STL MATCHES "_shared$")
set(stlLibName "${CMAKE_SHARED_LIBRARY_PREFIX}${ANDROID_STL}${CMAKE_SHARED_LIBRARY_SUFFIX}") set(stlLibName "${CMAKE_SHARED_LIBRARY_PREFIX}${ANDROID_STL}${CMAKE_SHARED_LIBRARY_SUFFIX}")
install(FILES "${CMAKE_SYSROOT}/usr/lib/${ANDROID_SYSROOT_LIB_SUBDIR}/${stlLibName}" install(FILES "${CMAKE_SYSROOT}/usr/lib/${ANDROID_SYSROOT_LIB_SUBDIR}/${stlLibName}"
DESTINATION ${LIB_DIR} DESTINATION ${LIB_DIR}
) )
endif() endif()
# zip internal assets - 'config' and 'Mods' dirs, save md5 of the zip
install(CODE "
cmake_path(ABSOLUTE_PATH CMAKE_INSTALL_PREFIX
OUTPUT_VARIABLE absolute_install_prefix
)
set(absolute_data_dir \"\${absolute_install_prefix}/${DATA_DIR}\")
file(MAKE_DIRECTORY \"\${absolute_data_dir}\")
set(internal_data_zip \"\${absolute_data_dir}/internalData.zip\")
execute_process(COMMAND
\"${CMAKE_COMMAND}\" -E tar c \"\${internal_data_zip}\" --format=zip -- config Mods
WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"
)
file(MD5 \"\${internal_data_zip}\" internal_data_zip_md5)
file(WRITE \"\${absolute_data_dir}/internalDataHash.txt\"
\${internal_data_zip_md5}
)
")
else() else()
install(DIRECTORY config DESTINATION ${DATA_DIR}) install(DIRECTORY config DESTINATION ${DATA_DIR})
if (ENABLE_CLIENT OR ENABLE_SERVER) if (ENABLE_CLIENT OR ENABLE_SERVER)

View File

@ -395,6 +395,9 @@ assign_source_group(${client_SRCS} ${client_HEADERS} VCMI_client.rc)
if(ANDROID) if(ANDROID)
add_library(vcmiclient SHARED ${client_SRCS} ${client_HEADERS}) add_library(vcmiclient SHARED ${client_SRCS} ${client_HEADERS})
set_target_properties(vcmiclient PROPERTIES
OUTPUT_NAME "vcmiclient_${ANDROID_ABI}" # required by Qt
)
else() else()
add_executable(vcmiclient ${client_SRCS} ${client_HEADERS}) add_executable(vcmiclient ${client_SRCS} ${client_HEADERS})
endif() endif()
@ -517,11 +520,19 @@ if(APPLE_IOS)
) )
install(TARGETS vcmiclient DESTINATION Payload COMPONENT app) # for ipa generation with cpack install(TARGETS vcmiclient DESTINATION Payload COMPONENT app) # for ipa generation with cpack
elseif(ANDROID) elseif(ANDROID)
vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}/${LIB_DIR}") find_program(androidDeployQt androiddeployqt
add_custom_command(TARGET vcmiclient POST_BUILD PATHS "${qtBinDir}"
COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --config "$<CONFIG>" --prefix "${CMAKE_SOURCE_DIR}/android/vcmi-app/src/main"
) )
install(TARGETS vcmiclient DESTINATION ${LIB_DIR}) vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
add_custom_target(android_deploy ALL
COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --config "$<CONFIG>" --prefix "${androidQtBuildDir}"
COMMAND "${androidDeployQt}" --input "${CMAKE_BINARY_DIR}/androiddeployqt.json" --output "${androidQtBuildDir}" --android-platform "android-${ANDROID_TARGET_SDK_VERSION}" --verbose $<$<NOT:$<CONFIG:Debug>>:--release> ${ANDROIDDEPLOYQT_OPTIONS}
COMMAND_EXPAND_LISTS
VERBATIM
COMMENT "Create android package"
)
add_dependencies(android_deploy vcmiclient)
else() else()
install(TARGETS vcmiclient DESTINATION ${BIN_DIR}) install(TARGETS vcmiclient DESTINATION ${BIN_DIR})
endif() endif()

View File

@ -7,16 +7,22 @@
macro(vcmi_set_output_dir name dir) macro(vcmi_set_output_dir name dir)
# Multi-config builds for Visual Studio, Xcode # Multi-config builds for Visual Studio, Xcode
foreach(OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES}) foreach(OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES})
string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIGUPPERCASE) string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIGUPPERCASE)
set_target_properties(${name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} ${CMAKE_BINARY_DIR}/bin/${OUTPUTCONFIG}/${dir}) set_target_properties(${name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} ${CMAKE_BINARY_DIR}/bin/${OUTPUTCONFIG}/${dir})
set_target_properties(${name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} ${CMAKE_BINARY_DIR}/bin/${OUTPUTCONFIG}/${dir}) if(ANDROID)
set_target_properties(${name} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} ${CMAKE_BINARY_DIR}/bin/${OUTPUTCONFIG}/${dir}) set_target_properties(${name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
else()
set_target_properties(${name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} ${CMAKE_BINARY_DIR}/bin/${OUTPUTCONFIG}/${dir})
endif()
set_target_properties(${name} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIGUPPERCASE} ${CMAKE_BINARY_DIR}/bin/${OUTPUTCONFIG}/${dir})
endforeach() endforeach()
# Generic no-config case for Makefiles, Ninja. # Generic no-config case for Makefiles, Ninja.
# This is what Qt Creator is using # This is what Qt Creator is using
set_target_properties(${name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${dir}) set_target_properties(${name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${dir})
set_target_properties(${name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${dir}) if(NOT ANDROID)
set_target_properties(${name} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${dir})
endif()
set_target_properties(${name} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${dir}) set_target_properties(${name} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${dir})
endmacro() endmacro()

View File

@ -21,7 +21,13 @@ set(launcher_SRCS
launcherdirs.cpp launcherdirs.cpp
jsonutils.cpp jsonutils.cpp
updatedialog_moc.cpp updatedialog_moc.cpp
prepare.cpp
) )
if(APPLE_IOS)
list(APPEND launcher_SRCS
ios/launchGame.m
)
endif()
set(launcher_HEADERS set(launcher_HEADERS
StdInc.h StdInc.h
@ -40,7 +46,8 @@ set(launcher_HEADERS
jsonutils.h jsonutils.h
updatedialog_moc.h updatedialog_moc.h
main.h main.h
helper.cpp helper.h
prepare.h
) )
set(launcher_FORMS set(launcher_FORMS
@ -53,30 +60,60 @@ set(launcher_FORMS
updatedialog_moc.ui updatedialog_moc.ui
) )
set(launcher_TS set(launcher_RESOURCES
translation/chinese.ts resources.qrc
translation/czech.ts
translation/english.ts
translation/french.ts
translation/german.ts
translation/polish.ts
translation/portuguese.ts
translation/russian.ts
translation/spanish.ts
translation/ukrainian.ts
translation/vietnamese.ts
) )
if(APPLE_IOS) set(translationsDir "translation")
list(APPEND launcher_SRCS set(launcher_TS
ios/main.m "${translationsDir}/chinese.ts"
) "${translationsDir}/czech.ts"
"${translationsDir}/english.ts"
"${translationsDir}/french.ts"
"${translationsDir}/german.ts"
"${translationsDir}/polish.ts"
"${translationsDir}/portuguese.ts"
"${translationsDir}/russian.ts"
"${translationsDir}/spanish.ts"
"${translationsDir}/ukrainian.ts"
"${translationsDir}/vietnamese.ts"
)
if(ENABLE_TRANSLATIONS)
if(TARGET Qt5::Core)
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${translationsDir}")
set_source_files_properties(${launcher_TS} PROPERTIES OUTPUT_LOCATION "${translationsDir}")
qt5_add_translation(launcher_QM ${launcher_TS})
set(translationsResource "${CMAKE_CURRENT_BINARY_DIR}/translations.qrc")
list(APPEND launcher_RESOURCES "${translationsResource}")
set(rccQmFiles "")
foreach(qmFile ${launcher_QM})
string(APPEND rccQmFiles "<file>${qmFile}</file>\n")
endforeach()
file(WRITE "${translationsResource}"
"<!DOCTYPE RCC>
<RCC version=\"1.0\">
<qresource prefix=\"/\">
${rccQmFiles}
</qresource>
</RCC>"
)
endif()
endif() endif()
assign_source_group(${launcher_SRCS} ${launcher_HEADERS} VCMI_launcher.rc) if(WIN32)
set(launcher_ICON VCMI_launcher.rc)
endif()
# Tell CMake to run moc when necessary: assign_source_group(${launcher_SRCS} ${launcher_HEADERS} ${launcher_RESOURCES} ${launcher_TS} ${launcher_ICON})
# TODO: enabling AUTORCC breaks msvc build on CI
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
if(NOT (MSVC AND "$ENV{GITHUB_ACTIONS}" STREQUAL true))
set(CMAKE_AUTORCC ON)
endif()
if(POLICY CMP0071) if(POLICY CMP0071)
cmake_policy(SET CMP0071 NEW) cmake_policy(SET CMP0071 NEW)
@ -86,38 +123,55 @@ endif()
# to always look for includes there: # to always look for includes there:
set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_INCLUDE_CURRENT_DIR ON)
if(TARGET Qt6::Core) if(ENABLE_SINGLE_APP_BUILD OR ANDROID)
qt_wrap_ui(launcher_UI_HEADERS ${launcher_FORMS}) add_library(vcmilauncher OBJECT ${launcher_QM})
else() else()
qt5_wrap_ui(launcher_UI_HEADERS ${launcher_FORMS}) add_executable(vcmilauncher WIN32 ${launcher_QM} ${launcher_ICON})
if(ENABLE_TRANSLATIONS)
set_source_files_properties(${launcher_TS} PROPERTIES OUTPUT_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/translation)
qt5_add_translation( launcher_QM ${launcher_TS} )
endif()
endif() endif()
if(ENABLE_TRANSLATIONS)
if(WIN32) if(TARGET Qt6::Core)
set(launcher_ICON VCMI_launcher.rc)
endif()
if(ENABLE_SINGLE_APP_BUILD)
add_library(vcmilauncher STATIC ${launcher_QM} ${launcher_SRCS} ${launcher_HEADERS} ${launcher_UI_HEADERS})
else()
add_executable(vcmilauncher WIN32 ${launcher_QM} ${launcher_SRCS} ${launcher_HEADERS} ${launcher_UI_HEADERS} ${launcher_ICON})
endif()
if(TARGET Qt6::Core)
if(ENABLE_TRANSLATIONS)
set_source_files_properties(${launcher_TS} PROPERTIES OUTPUT_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/translation)
qt_add_translations(vcmilauncher qt_add_translations(vcmilauncher
TS_FILES ${launcher_TS} TS_FILES ${launcher_TS}
QM_FILES_OUTPUT_VARIABLE launcher_QM RESOURCE_PREFIX "/${translationsDir}"
INCLUDE_DIRECTORIES INCLUDE_DIRECTORIES
${CMAKE_CURRENT_BINARY_DIR}) ${CMAKE_CURRENT_BINARY_DIR})
endif() endif()
endif() endif()
if(ANDROID)
get_target_property(rccPath Qt${QT_VERSION_MAJOR}::rcc IMPORTED_LOCATION)
get_filename_component(qtDir "${rccPath}/../../" ABSOLUTE)
set(qtDir "${qtDir}" PARENT_SCOPE)
function(generate_binary_resource resourceName resourceDir)
file(CREATE_LINK "${resourceDir}" "${CMAKE_CURRENT_BINARY_DIR}/${resourceName}"
COPY_ON_ERROR
SYMBOLIC
)
set(qrcFile "${CMAKE_CURRENT_BINARY_DIR}/${resourceName}.qrc")
execute_process(COMMAND
"${rccPath}" --project
WORKING_DIRECTORY "${resourceDir}"
OUTPUT_VARIABLE rccOutput
)
# add parent directory
string(REPLACE "<file>." "<file>${resourceName}" rccOutput "${rccOutput}")
file(WRITE "${qrcFile}" "${rccOutput}")
endfunction()
generate_binary_resource("config" "${CMAKE_SOURCE_DIR}/config")
list(APPEND launcher_RESOURCES "${CMAKE_CURRENT_BINARY_DIR}/config.qrc")
generate_binary_resource("Mods" "${CMAKE_SOURCE_DIR}/Mods")
list(APPEND launcher_RESOURCES "${CMAKE_CURRENT_BINARY_DIR}/Mods.qrc")
endif()
target_sources(vcmilauncher PRIVATE
${launcher_SRCS}
${launcher_HEADERS}
${launcher_RESOURCES}
)
if(WIN32) if(WIN32)
set_target_properties(vcmilauncher set_target_properties(vcmilauncher
PROPERTIES PROPERTIES
@ -139,7 +193,9 @@ if(APPLE)
set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER vcmilauncher) set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER vcmilauncher)
endif() endif()
if (NOT APPLE_IOS AND NOT ANDROID) if(ANDROID)
target_link_libraries(vcmilauncher Qt${QT_VERSION_MAJOR}::AndroidExtras)
elseif(NOT APPLE_IOS)
target_link_libraries(vcmilauncher SDL2::SDL2) target_link_libraries(vcmilauncher SDL2::SDL2)
endif() endif()
@ -155,9 +211,7 @@ if(ENABLE_INNOEXTRACT)
endif() endif()
if(APPLE_IOS) if(APPLE_IOS)
set(RESOURCES_DESTINATION ${DATA_DIR}) # TODO: remove after switching prebuilt deps to a newer Conan's Qt recipe
# TODO: remove after fixing Conan's Qt recipe
if(XCODE_VERSION VERSION_GREATER_EQUAL 14.0) if(XCODE_VERSION VERSION_GREATER_EQUAL 14.0)
target_link_libraries(vcmilauncher "-framework IOKit") target_link_libraries(vcmilauncher "-framework IOKit")
endif() endif()
@ -167,22 +221,19 @@ if(APPLE_IOS)
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/QIOSIntegrationPlugin.h file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/QIOSIntegrationPlugin.h
"#include <QtPlugin>\nQ_IMPORT_PLUGIN(QIOSIntegrationPlugin)" "#include <QtPlugin>\nQ_IMPORT_PLUGIN(QIOSIntegrationPlugin)"
) )
# target_include_directories(vcmilauncher PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(vcmilauncher target_link_libraries(vcmilauncher
Qt${QT_VERSION_MAJOR}::QIOSIntegrationPlugin Qt${QT_VERSION_MAJOR}::QIOSIntegrationPlugin
qt::QIOSIntegrationPlugin qt::QIOSIntegrationPlugin
) )
endif() endif()
else() elseif(ANDROID)
set(RESOURCES_DESTINATION ${DATA_DIR}/launcher) set(androidSdkDir "$ENV{ANDROID_HOME}")
configure_file(
# Link to build directory for easier debugging "${androidPackageSourceDir}/androiddeployqt.json.in"
add_custom_command(TARGET vcmilauncher POST_BUILD "${CMAKE_BINARY_DIR}/androiddeployqt.json"
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher @ONLY
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_SOURCE_DIR}/launcher/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/translation
) )
else()
install(TARGETS vcmilauncher DESTINATION ${BIN_DIR}) install(TARGETS vcmilauncher DESTINATION ${BIN_DIR})
# Install icons and desktop file on Linux # Install icons and desktop file on Linux
@ -191,8 +242,3 @@ else()
install(FILES "eu.vcmi.VCMI.metainfo.xml" DESTINATION share/metainfo) install(FILES "eu.vcmi.VCMI.metainfo.xml" DESTINATION share/metainfo)
endif() endif()
endif() endif()
install(DIRECTORY icons DESTINATION ${RESOURCES_DESTINATION})
if(ENABLE_TRANSLATIONS)
install(FILES ${launcher_QM} DESTINATION ${RESOURCES_DESTINATION}/translation)
endif()

View File

@ -363,7 +363,7 @@ void FirstLaunchView::extractGogData()
void FirstLaunchView::copyHeroesData(const QString & path, bool move) void FirstLaunchView::copyHeroesData(const QString & path, bool move)
{ {
QDir sourceRoot = QDir(path); QDir sourceRoot{path};
if(path.isEmpty()) if(path.isEmpty())
sourceRoot.setPath(QFileDialog::getExistingDirectory(this, {}, {}, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks)); sourceRoot.setPath(QFileDialog::getExistingDirectory(this, {}, {}, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks));
@ -387,9 +387,10 @@ void FirstLaunchView::copyHeroesData(const QString & path, bool move)
QStringList dirMaps = sourceRoot.entryList({"maps"}, QDir::Filter::Dirs); QStringList dirMaps = sourceRoot.entryList({"maps"}, QDir::Filter::Dirs);
QStringList dirMp3 = sourceRoot.entryList({"mp3"}, QDir::Filter::Dirs); QStringList dirMp3 = sourceRoot.entryList({"mp3"}, QDir::Filter::Dirs);
const auto noDataMessage = tr("Failed to detect valid Heroes III data in chosen directory.\nPlease select directory with installed Heroes III data.");
if(dirData.empty()) if(dirData.empty())
{ {
QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Failed to detect valid Heroes III data in chosen directory.\nPlease select directory with installed Heroes III data.")); QMessageBox::critical(this, tr("Heroes III data not found!"), noDataMessage);
return; return;
} }
@ -403,7 +404,7 @@ void FirstLaunchView::copyHeroesData(const QString & path, bool move)
if (roeFiles.empty()) if (roeFiles.empty())
{ {
// Directory structure is correct (Data/Maps/Mp3) but no .lod archives that should be present in any install // Directory structure is correct (Data/Maps/Mp3) but no .lod archives that should be present in any install
QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Failed to detect valid Heroes III data in chosen directory.\nPlease select directory with installed Heroes III data.")); QMessageBox::critical(this, tr("Heroes III data not found!"), noDataMessage);
return; return;
} }

View File

@ -96,5 +96,4 @@ private slots:
private: private:
Ui::FirstLaunchView * ui; Ui::FirstLaunchView * ui;
}; };

View File

@ -10,24 +10,35 @@
#include "StdInc.h" #include "StdInc.h"
#include "main.h" #include "main.h"
#include "mainwindow_moc.h" #include "mainwindow_moc.h"
#include "launcherdirs.h" #include "prepare.h"
#include "../lib/VCMIDirs.h" #include "../lib/VCMIDirs.h"
#include <QApplication> #include <QApplication>
#include <QProcess>
#include <QMessageBox>
// Conan workaround https://github.com/conan-io/conan-center-index/issues/13332 // Conan workaround https://github.com/conan-io/conan-center-index/issues/13332
#ifdef VCMI_IOS #ifdef VCMI_IOS
#if __has_include("QIOSIntegrationPlugin.h") # if __has_include("QIOSIntegrationPlugin.h")
#include "QIOSIntegrationPlugin.h" # include "QIOSIntegrationPlugin.h"
#endif # endif
int argcForClient; int argcForClient;
char ** argvForClient; char ** argvForClient;
#endif #elif defined(VCMI_ANDROID)
# include <QAndroidJniObject>
# include <QtAndroid>
#else
# include <QMessageBox>
# include <QProcess>
#endif // VCMI_IOS
int main(int argc, char * argv[]) // android must export main explicitly to make it visible in the shared library
#ifdef VCMI_ANDROID
# define MAIN_EXPORT ELF_VISIBILITY
#else
# define MAIN_EXPORT
#endif // VCMI_ANDROID
int MAIN_EXPORT main(int argc, char * argv[])
{ {
int result; int result;
#ifdef VCMI_IOS #ifdef VCMI_IOS
@ -35,7 +46,7 @@ int main(int argc, char * argv[])
#endif #endif
QApplication vcmilauncher(argc, argv); QApplication vcmilauncher(argc, argv);
CLauncherDirs::prepare(); launcher::prepare();
MainWindow mainWindow; MainWindow mainWindow;
mainWindow.show(); mainWindow.show();
@ -53,7 +64,7 @@ void startGame(const QStringList & args)
{ {
logGlobal->warn("Starting game with the arguments: %s", args.join(" ").toStdString()); logGlobal->warn("Starting game with the arguments: %s", args.join(" ").toStdString());
#ifdef Q_OS_IOS #ifdef VCMI_IOS
static const char clientName[] = "vcmiclient"; static const char clientName[] = "vcmiclient";
argcForClient = args.size() + 1; //first argument is omitted argcForClient = args.size() + 1; //first argument is omitted
argvForClient = new char*[argcForClient]; argvForClient = new char*[argcForClient];
@ -61,11 +72,13 @@ void startGame(const QStringList & args)
strcpy(argvForClient[0], clientName); strcpy(argvForClient[0], clientName);
for(int i = 1; i < argcForClient; ++i) for(int i = 1; i < argcForClient; ++i)
{ {
std::string s = args.at(i - 1).toStdString(); std::string s = args.at(i - 1).toStdString();
argvForClient[i] = new char[s.size() + 1]; argvForClient[i] = new char[s.size() + 1];
strcpy(argvForClient[i], s.c_str()); strcpy(argvForClient[i], s.c_str());
} }
qApp->quit(); qApp->quit();
#elif defined(VCMI_ANDROID)
QtAndroid::androidActivity().callMethod<void>("onLaunchGameBtnPressed");
#else #else
startExecutable(pathToQString(VCMIDirs::get().clientPath()), args); startExecutable(pathToQString(VCMIDirs::get().clientPath()), args);
#endif #endif
@ -78,7 +91,7 @@ void startEditor(const QStringList & args)
#endif #endif
} }
#ifndef Q_OS_IOS #ifndef VCMI_MOBILE
void startExecutable(QString name, const QStringList & args) void startExecutable(QString name, const QStringList & args)
{ {
QProcess process; QProcess process;
@ -91,11 +104,9 @@ void startExecutable(QString name, const QStringList & args)
else else
{ {
QMessageBox::critical(qApp->activeWindow(), QMessageBox::critical(qApp->activeWindow(),
"Error starting executable", QObject::tr("Error starting executable"),
"Failed to start " + name + "\n" QObject::tr("Failed to start %1\nReason: %2").arg(name, process.errorString())
"Reason: " + process.errorString(), );
QMessageBox::Ok,
QMessageBox::Ok);
} }
} }
#endif #endif

View File

@ -29,7 +29,7 @@ void MainWindow::load()
// This is important on Mac for relative paths to work inside DMG. // This is important on Mac for relative paths to work inside DMG.
QDir::setCurrent(QApplication::applicationDirPath()); QDir::setCurrent(QApplication::applicationDirPath());
#ifndef VCMI_IOS #ifndef VCMI_MOBILE
console = new CConsoleHandler(); console = new CConsoleHandler();
#endif #endif
CBasicLogConfigurator logConfig(VCMIDirs::get().userLogsPath() / "VCMI_Launcher_log.txt", console); CBasicLogConfigurator logConfig(VCMIDirs::get().userLogsPath() / "VCMI_Launcher_log.txt", console);
@ -38,14 +38,6 @@ void MainWindow::load()
CResourceHandler::initialize(); CResourceHandler::initialize();
CResourceHandler::load("config/filesystem.json"); CResourceHandler::load("config/filesystem.json");
#ifdef Q_OS_IOS
QDir::addSearchPath("icons", pathToQString(VCMIDirs::get().binaryPath() / "icons"));
#else
for(auto & string : VCMIDirs::get().dataPaths())
QDir::addSearchPath("icons", pathToQString(string / "launcher" / "icons"));
QDir::addSearchPath("icons", pathToQString(VCMIDirs::get().userDataPath() / "launcher" / "icons"));
#endif
Helper::loadSettings(); Helper::loadSettings();
} }
@ -85,7 +77,15 @@ MainWindow::MainWindow(QWidget * parent)
updateTranslation(); // load translation updateTranslation(); // load translation
ui->setupUi(this); ui->setupUi(this);
setWindowIcon(QIcon{":/icons/menu-game.png"});
ui->modslistButton->setIcon(QIcon{":/icons/menu-mods.png"});
ui->settingsButton->setIcon(QIcon{":/icons/menu-settings.png"});
ui->aboutButton->setIcon(QIcon{":/icons/about-project.png"});
ui->startEditorButton->setIcon(QIcon{":/icons/menu-editor.png"});
ui->startGameButton->setIcon(QIcon{":/icons/menu-game.png"});
#ifndef VCMI_MOBILE
//load window settings //load window settings
QSettings s(Ui::teamName, Ui::appName); QSettings s(Ui::teamName, Ui::appName);
@ -99,6 +99,7 @@ MainWindow::MainWindow(QWidget * parent)
{ {
move(position); move(position);
} }
#endif
#ifndef ENABLE_EDITOR #ifndef ENABLE_EDITOR
ui->startEditorButton->hide(); ui->startEditorButton->hide();
@ -183,10 +184,12 @@ void MainWindow::changeEvent(QEvent *event)
MainWindow::~MainWindow() MainWindow::~MainWindow()
{ {
#ifndef VCMI_MOBILE
//save window settings //save window settings
QSettings s(Ui::teamName, Ui::appName); QSettings s(Ui::teamName, Ui::appName);
s.setValue("MainWindow/Size", size()); s.setValue("MainWindow/Size", size());
s.setValue("MainWindow/Position", pos()); s.setValue("MainWindow/Position", pos());
#endif
delete ui; delete ui;
} }
@ -231,32 +234,16 @@ void MainWindow::on_aboutButton_clicked()
void MainWindow::updateTranslation() void MainWindow::updateTranslation()
{ {
#ifdef ENABLE_QT_TRANSLATIONS #ifdef ENABLE_QT_TRANSLATIONS
std::string translationFile = settings["general"]["language"].String() + ".qm"; const std::string translationFile = settings["general"]["language"].String() + ".qm";
logGlobal->info("Loading translation '%s'", translationFile); logGlobal->info("Loading translation '%s'", translationFile);
QVector<QString> searchPaths; if (!translator.load(QString{":/translation/%1"}.arg(translationFile.c_str())))
#ifdef Q_OS_IOS
searchPaths.push_back(pathToQString(VCMIDirs::get().binaryPath() / "translation" / translationFile));
#else
for(auto const & string : VCMIDirs::get().dataPaths())
searchPaths.push_back(pathToQString(string / "launcher" / "translation" / translationFile));
searchPaths.push_back(pathToQString(VCMIDirs::get().userDataPath() / "launcher" / "translation" / translationFile));
#endif
for(auto const & string : boost::adaptors::reverse(searchPaths))
{ {
logGlobal->info("Searching for translation at '%s'", string.toStdString()); logGlobal->error("Failed to load translation");
if (translator.load(string)) return;
{
logGlobal->info("Translation found");
if (!qApp->installTranslator(&translator))
logGlobal->error("Failed to install translator");
return;
}
} }
logGlobal->error("Failed to find translation"); if (!qApp->installTranslator(&translator))
logGlobal->error("Failed to install translator");
#endif #endif
} }

View File

@ -19,10 +19,6 @@
<property name="windowTitle"> <property name="windowTitle">
<string>VCMI Launcher</string> <string>VCMI Launcher</string>
</property> </property>
<property name="windowIcon">
<iconset>
<normaloff>icons:menu-game.png</normaloff>icons:menu-game.png</iconset>
</property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
<width>64</width> <width>64</width>
@ -56,10 +52,6 @@
<property name="text"> <property name="text">
<string>Mods</string> <string>Mods</string>
</property> </property>
<property name="icon">
<iconset>
<normaloff>icons:menu-mods.png</normaloff>icons:menu-mods.png</iconset>
</property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
<width>64</width> <width>64</width>
@ -106,10 +98,6 @@
<property name="text"> <property name="text">
<string>Settings</string> <string>Settings</string>
</property> </property>
<property name="icon">
<iconset>
<normaloff>icons:menu-settings.png</normaloff>icons:menu-settings.png</iconset>
</property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
<width>64</width> <width>64</width>
@ -156,10 +144,6 @@
<property name="text"> <property name="text">
<string>Help</string> <string>Help</string>
</property> </property>
<property name="icon">
<iconset>
<normaloff>icons:about-project.png</normaloff>icons:about-project.png</iconset>
</property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
<width>32</width> <width>32</width>
@ -225,10 +209,6 @@
<property name="text"> <property name="text">
<string>Map Editor</string> <string>Map Editor</string>
</property> </property>
<property name="icon">
<iconset>
<normaloff>icons:menu-editor.png</normaloff>icons:menu-editor.png</iconset>
</property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
<width>32</width> <width>32</width>
@ -278,10 +258,6 @@
<property name="text"> <property name="text">
<string>Start game</string> <string>Start game</string>
</property> </property>
<property name="icon">
<iconset>
<normaloff>icons:menu-game.png</normaloff>icons:menu-game.png</iconset>
</property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
<width>64</width> <width>64</width>

View File

@ -14,11 +14,11 @@
namespace ModStatus namespace ModStatus
{ {
static const QString iconDelete = "icons:mod-delete.png"; static const QString iconDelete = ":/icons/mod-delete.png";
static const QString iconDisabled = "icons:mod-disabled.png"; static const QString iconDisabled = ":/icons/mod-disabled.png";
static const QString iconDownload = "icons:mod-download.png"; static const QString iconDownload = ":/icons/mod-download.png";
static const QString iconEnabled = "icons:mod-enabled.png"; static const QString iconEnabled = ":/icons/mod-enabled.png";
static const QString iconUpdate = "icons:mod-update.png"; static const QString iconUpdate = ":/icons/mod-update.png";
} }
CModListModel::CModListModel(QObject * parent) CModListModel::CModListModel(QObject * parent)

View File

@ -130,6 +130,12 @@ CModListView::CModListView(QWidget * parent)
setAcceptDrops(true); setAcceptDrops(true);
ui->uninstallButton->setIcon(QIcon{":/icons/mod-delete.png"});
ui->enableButton->setIcon(QIcon{":/icons/mod-enabled.png"});
ui->disableButton->setIcon(QIcon{":/icons/mod-disabled.png"});
ui->updateButton->setIcon(QIcon{":/icons/mod-update.png"});
ui->installButton->setIcon(QIcon{":/icons/mod-download.png"});
setupModModel(); setupModModel();
setupFilterModel(); setupFilterModel();
setupModsView(); setupModsView();
@ -393,14 +399,15 @@ void CModListView::selectMod(const QModelIndex & index)
} }
else else
{ {
auto mod = modModel->getMod(index.data(ModRoles::ModNameRole).toString()); const auto modName = index.data(ModRoles::ModNameRole).toString();
auto mod = modModel->getMod(modName);
ui->modInfoBrowser->setHtml(genModInfoText(mod)); ui->modInfoBrowser->setHtml(genModInfoText(mod));
ui->changelogBrowser->setHtml(genChangelogText(mod)); ui->changelogBrowser->setHtml(genChangelogText(mod));
bool hasInvalidDeps = !findInvalidDependencies(index.data(ModRoles::ModNameRole).toString()).empty(); bool hasInvalidDeps = !findInvalidDependencies(modName).empty();
bool hasBlockingMods = !findBlockingMods(index.data(ModRoles::ModNameRole).toString()).empty(); bool hasBlockingMods = !findBlockingMods(modName).empty();
bool hasDependentMods = !findDependentMods(index.data(ModRoles::ModNameRole).toString(), true).empty(); bool hasDependentMods = !findDependentMods(modName, true).empty();
ui->disableButton->setVisible(mod.isEnabled()); ui->disableButton->setVisible(mod.isEnabled());
ui->enableButton->setVisible(mod.isDisabled()); ui->enableButton->setVisible(mod.isDisabled());

View File

@ -423,10 +423,6 @@ hr { height: 1px; border-width: 0; }
<property name="text"> <property name="text">
<string>Uninstall</string> <string>Uninstall</string>
</property> </property>
<property name="icon">
<iconset>
<normaloff>icons:mod-delete.png</normaloff>icons:mod-delete.png</iconset>
</property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
<width>20</width> <width>20</width>
@ -458,10 +454,6 @@ hr { height: 1px; border-width: 0; }
<property name="text"> <property name="text">
<string>Enable</string> <string>Enable</string>
</property> </property>
<property name="icon">
<iconset>
<normaloff>icons:mod-enabled.png</normaloff>icons:mod-enabled.png</iconset>
</property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
<width>20</width> <width>20</width>
@ -493,10 +485,6 @@ hr { height: 1px; border-width: 0; }
<property name="text"> <property name="text">
<string>Disable</string> <string>Disable</string>
</property> </property>
<property name="icon">
<iconset>
<normaloff>icons:mod-disabled.png</normaloff>icons:mod-disabled.png</iconset>
</property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
<width>20</width> <width>20</width>
@ -528,10 +516,6 @@ hr { height: 1px; border-width: 0; }
<property name="text"> <property name="text">
<string>Update</string> <string>Update</string>
</property> </property>
<property name="icon">
<iconset>
<normaloff>icons:mod-update.png</normaloff>icons:mod-update.png</iconset>
</property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
<width>20</width> <width>20</width>
@ -563,10 +547,6 @@ hr { height: 1px; border-width: 0; }
<property name="text"> <property name="text">
<string>Install</string> <string>Install</string>
</property> </property>
<property name="icon">
<iconset>
<normaloff>icons:mod-download.png</normaloff>icons:mod-download.png</iconset>
</property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
<width>20</width> <width>20</width>

View File

@ -20,6 +20,8 @@
#include "../jsonutils.h" #include "../jsonutils.h"
#include "../launcherdirs.h" #include "../launcherdirs.h"
#include <future>
namespace namespace
{ {
QString detectModArchive(QString path, QString modName, std::vector<std::string> & filesToExtract) QString detectModArchive(QString path, QString modName, std::vector<std::string> & filesToExtract)
@ -360,7 +362,7 @@ bool CModManager::removeModDir(QString path)
if(!checkDir.cdUp() || QString::compare("Mods", checkDir.dirName(), Qt::CaseInsensitive)) if(!checkDir.cdUp() || QString::compare("Mods", checkDir.dirName(), Qt::CaseInsensitive))
return false; return false;
#ifndef VCMI_IOS //ios applications are stored in the isolated container #ifndef VCMI_MOBILE // ios and android applications are stored in the isolated container
if(!checkDir.cdUp() || QString::compare("vcmi", checkDir.dirName(), Qt::CaseInsensitive)) if(!checkDir.cdUp() || QString::compare("vcmi", checkDir.dirName(), Qt::CaseInsensitive))
return false; return false;

85
launcher/prepare.cpp Normal file
View File

@ -0,0 +1,85 @@
/*
* prepare.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "prepare.h"
#include "launcherdirs.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#ifdef VCMI_ANDROID
#include "../lib/CAndroidVMHelper.h"
#include <QAndroidJniEnvironment>
#include <QAndroidJniObject>
#include <QtAndroid>
namespace
{
// https://gist.github.com/ssendeavour/7324701
bool copyRecursively(const QString &srcFilePath, const QString &tgtFilePath)
{
QFileInfo srcFileInfo{srcFilePath};
if(srcFileInfo.isDir()) {
QDir targetDir{tgtFilePath};
targetDir.cdUp();
if(!targetDir.mkpath(QFileInfo{tgtFilePath}.fileName()))
return false;
targetDir.setPath(tgtFilePath);
QDir sourceDir{srcFilePath};
const auto fileNames = sourceDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
for(const auto & fileName : fileNames) {
const auto newSrcFilePath = sourceDir.filePath(fileName);
const auto newTgtFilePath = targetDir.filePath(fileName);
if(!copyRecursively(newSrcFilePath, newTgtFilePath))
return false;
}
} else {
if(!QFile::copy(srcFilePath, tgtFilePath))
return false;
}
return true;
}
void prepareAndroid()
{
QAndroidJniEnvironment jniEnv;
CAndroidVMHelper::initClassloader(static_cast<JNIEnv *>(jniEnv));
const bool justLaunched = QtAndroid::androidActivity().getField<jboolean>("justLaunched") == JNI_TRUE;
if(!justLaunched)
return;
// copy core data to internal directory
const auto vcmiDir = QAndroidJniObject::callStaticObjectMethod<jstring>("eu/vcmi/vcmi/NativeMethods", "internalDataRoot").toString();
for(auto vcmiFilesResource : {QLatin1String{"config"}, QLatin1String{"Mods"}})
{
QDir destDir = QString{"%1/%2"}.arg(vcmiDir, vcmiFilesResource);
destDir.removeRecursively();
copyRecursively(QString{":/%1"}.arg(vcmiFilesResource), destDir.absolutePath());
}
}
}
#endif
namespace launcher
{
void prepare()
{
#ifdef VCMI_ANDROID
prepareAndroid();
#endif
CLauncherDirs::prepare();
}
}

15
launcher/prepare.h Normal file
View File

@ -0,0 +1,15 @@
/*
* prepare.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
namespace launcher
{
void prepare();
}

14
launcher/resources.qrc Normal file
View File

@ -0,0 +1,14 @@
<RCC>
<qresource prefix="/">
<file>icons/about-project.png</file>
<file>icons/menu-editor.png</file>
<file>icons/menu-game.png</file>
<file>icons/menu-mods.png</file>
<file>icons/menu-settings.png</file>
<file>icons/mod-delete.png</file>
<file>icons/mod-disabled.png</file>
<file>icons/mod-download.png</file>
<file>icons/mod-enabled.png</file>
<file>icons/mod-update.png</file>
</qresource>
</RCC>

View File

@ -7,7 +7,6 @@ if(APPLE_MACOS)
if(ENABLE_LAUNCHER OR ENABLE_EDITOR) if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
if(USING_CONAN) if(USING_CONAN)
# simulate macdeployqt behavior, main Qt libs are copied by conan # simulate macdeployqt behavior, main Qt libs are copied by conan
get_target_property(qmakePath Qt5::qmake IMPORTED_LOCATION)
execute_process(COMMAND execute_process(COMMAND
"${qmakePath}" -query QT_INSTALL_PLUGINS "${qmakePath}" -query QT_INSTALL_PLUGINS
OUTPUT_VARIABLE qtPluginsDir OUTPUT_VARIABLE qtPluginsDir
@ -25,7 +24,7 @@ if(APPLE_MACOS)
else() else()
# note: cross-compiled Qt 5 builds macdeployqt for target platform instead of host # note: cross-compiled Qt 5 builds macdeployqt for target platform instead of host
# deploy Qt dylibs with macdeployqt # deploy Qt dylibs with macdeployqt
find_program(TOOL_MACDEPLOYQT NAMES macdeployqt PATHS ${qt_base_dir}/bin) find_program(TOOL_MACDEPLOYQT NAMES macdeployqt PATHS "${qtBinDir}")
if(TOOL_MACDEPLOYQT) if(TOOL_MACDEPLOYQT)
install(CODE " install(CODE "
execute_process(COMMAND execute_process(COMMAND