WIP: Add CLI-wrapper for Windows

Starting the console from a GUI application is not working very
well - so let's just provide a 2nd executable for the CLI. It
will be a simple console application that merely invokes the main
application passing all standard I/O.
This commit is contained in:
Martchus 2023-05-06 22:48:27 +02:00
parent 5425488421
commit f3b0be6620
6 changed files with 176 additions and 9 deletions

View File

@ -98,9 +98,15 @@ set(CMAKE_TEMPLATE_FILES
cmake/templates/template.pc.in)
set(SCRIPT_FILES)
if (MINGW)
list(APPEND CMAKE_TEMPLATE_FILES cmake/templates/windows.rc.in)
list(APPEND CMAKE_TEMPLATE_FILES
cmake/templates/windows.rc.in
cmake/templates/windows-cli-wrapper.rc.in
)
list(APPEND SCRIPT_FILES scripts/wine.sh)
endif ()
if (WIN32)
list(APPEND CMAKE_TEMPLATE_FILES cmake/templates/cli-wrapper.cpp)
endif ()
set(DOC_FILES README.md doc/buildvariables.md doc/testapplication.md)
set(EXTRA_FILES tests/calculateoverallcoverage.awk coding-style.clang-format)

View File

@ -110,6 +110,38 @@ if (GUI_TYPE STREQUAL "MACOSX_BUNDLE")
endif ()
endif ()
# create CLI-wrapper to be able to use CLI in Windows-termial without hacks
if (GUI_TYPE STREQUAL "WIN32")
option(BUILD_CLI_WRAPPER "whether to build a CLI wrapper" ON)
endif ()
if (BUILD_CLI_WRAPPER)
set(CLI_WRAPPER_TARGET_NAME "${META_TARGET_NAME}-cli")
# add Boost::boost target which represents include directory for header-only deps and add Boost::filesystem as it is
# needed by Boost.Process
set(BOOST_ARGS 1.75 REQUIRED COMPONENTS filesystem)
set(USE_PACKAGE_ARGS
LIBRARIES_VARIABLE CLI_WRAPPER_TARGET_NAME_LIBS
PACKAGES_VARIABLE CLI_WRAPPER_TARGET_NAME_PKGS)
use_package(TARGET_NAME Boost::boost PACKAGE_NAME Boost PACKAGE_ARGS "${BOOST_ARGS}" ${USE_PACKAGE_ARGS})
use_package(TARGET_NAME Boost::filesystem PACKAGE_NAME Boost PACKAGE_ARGS "${BOOST_ARGS}" ${USE_PACKAGE_ARGS})
# find source file
include(TemplateFinder)
find_template_file_full_name("cli-wrapper.cpp" CPP_UTILITIES CLI_WRAPPER_SRC_FILE)
# add and configure additional executable
add_executable(${CLI_WRAPPER_TARGET_NAME} ${CLI_WRAPPER_RES_FILES} ${CLI_WRAPPER_SRC_FILE})
target_link_libraries(${CLI_WRAPPER_TARGET_NAME} PRIVATE "${CLI_WRAPPER_TARGET_NAME_LIBS}")
if (MINGW)
# workaround https://github.com/boostorg/process/issues/96
target_compile_definitions(${CLI_WRAPPER_TARGET_NAME} PRIVATE BOOST_USE_WINDOWS_H WIN32_LEAN_AND_MEAN)
elseif (MSVC)
# prevent "Please define _WIN32_WINNT or _WIN32_WINDOWS appropriately."
target_compile_definitions(${CLI_WRAPPER_TARGET_NAME} PRIVATE _WIN32_WINNT=0x0601)
endif ()
endif ()
# add install targets
if (NOT META_NO_INSTALL_TARGETS AND ENABLE_INSTALL_TARGETS)
# add install target for binary
@ -129,6 +161,9 @@ if (NOT META_NO_INSTALL_TARGETS AND ENABLE_INSTALL_TARGETS)
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}${SELECTED_LIB_SUFFIX}" COMPONENT binary)
else ()
install(TARGETS ${META_TARGET_NAME} RUNTIME DESTINATION bin COMPONENT binary)
if (CLI_WRAPPER_TARGET_NAME)
install(TARGETS ${CLI_WRAPPER_TARGET_NAME} RUNTIME DESTINATION bin COMPONENT binary)
endif ()
endif ()
if (NOT TARGET install-binary)

View File

@ -6,23 +6,28 @@ if (DEFINED TEMPLATE_FINDER_LOADED)
endif ()
set(TEMPLATE_FINDER_LOADED YES)
function (find_template_file FILE_NAME PROJECT_VAR_NAME OUTPUT_VAR)
if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${FILE_NAME}.in")
function (find_template_file FILE_NAME_WITHOUT_EXTENSION PROJECT_VAR_NAME OUTPUT_VAR)
find_template_file_full_name("${FILE_NAME_WITHOUT_EXTENSION}.in" "${PROJECT_VAR_NAME}" "${OUTPUT_VAR}")
set(${OUTPUT_VAR} "${${OUTPUT_VAR}}" PARENT_SCOPE)
endfunction ()
function (find_template_file_full_name FILE_NAME PROJECT_VAR_NAME OUTPUT_VAR)
if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${FILE_NAME}")
# check own source directory
set(${OUTPUT_VAR}
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${FILE_NAME}.in"
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${FILE_NAME}"
PARENT_SCOPE)
message(STATUS "Using template for ${FILE_NAME} from own (${META_PROJECT_NAME}) source directory.")
elseif (EXISTS "${${PROJECT_VAR_NAME}_SOURCE_DIR}/cmake/templates/${FILE_NAME}.in")
elseif (EXISTS "${${PROJECT_VAR_NAME}_SOURCE_DIR}/cmake/templates/${FILE_NAME}")
# check sources of project
set(${OUTPUT_VAR}
"${${PROJECT_VAR_NAME}_SOURCE_DIR}/cmake/templates/${FILE_NAME}.in"
"${${PROJECT_VAR_NAME}_SOURCE_DIR}/cmake/templates/${FILE_NAME}"
PARENT_SCOPE)
message(STATUS "Using template for ${FILE_NAME} from ${PROJECT_VAR_NAME} source directory.")
elseif (EXISTS "${${PROJECT_VAR_NAME}_CONFIG_DIRS}/templates/${FILE_NAME}.in")
elseif (EXISTS "${${PROJECT_VAR_NAME}_CONFIG_DIRS}/templates/${FILE_NAME}")
# check installed version of project
set(${OUTPUT_VAR}
"${${PROJECT_VAR_NAME}_CONFIG_DIRS}/templates/${FILE_NAME}.in"
"${${PROJECT_VAR_NAME}_CONFIG_DIRS}/templates/${FILE_NAME}"
PARENT_SCOPE)
message(STATUS "Using template for ${FILE_NAME} from ${PROJECT_VAR_NAME} installation.")
else ()

View File

@ -19,8 +19,9 @@ endif ()
# find rc template, define path of output rc file
include(TemplateFinder)
find_template_file("windows.rc" CPP_UTILITIES RC_TEMPLATE_FILE)
set(WINDOWS_RC_FILE_CFG "${CMAKE_CURRENT_BINARY_DIR}/resources/windows.rc.configured")
find_template_file("windows-cli-wrapper.rc" CPP_UTILITIES RC_CLI_TEMPLATE_FILE)
set(WINDOWS_RC_FILE "${CMAKE_CURRENT_BINARY_DIR}/resources/windows")
set(WINDOWS_CLI_RC_FILE "${CMAKE_CURRENT_BINARY_DIR}/resources/windows-cli-wrapper")
# create Windows icon from png with ffmpeg if available
unset(WINDOWS_ICON_RC_ENTRY)
@ -51,10 +52,17 @@ file(
GENERATE
OUTPUT "${WINDOWS_RC_FILE}-$<CONFIG>.rc"
INPUT "${WINDOWS_RC_FILE}-configured.rc")
configure_file("${RC_CLI_TEMPLATE_FILE}" "${WINDOWS_CLI_RC_FILE}-configured.rc")
file(
GENERATE
OUTPUT "${WINDOWS_CLI_RC_FILE}-$<CONFIG>.rc"
INPUT "${WINDOWS_CLI_RC_FILE}-configured.rc")
# set windres as resource compiler
list(APPEND RES_FILES "${WINDOWS_RC_FILE}-${CMAKE_BUILD_TYPE}.rc")
list(APPEND CLI_WRAPPER_RES_FILES "${WINDOWS_CLI_RC_FILE}-${CMAKE_BUILD_TYPE}.rc")
set_property(SOURCE "${WINDOWS_RC_FILE}-${CMAKE_BUILD_TYPE}.rc" PROPERTY GENERATED ON)
set_property(SOURCE "${WINDOWS_CLI_RC_FILE}-${CMAKE_BUILD_TYPE}.rc" PROPERTY GENERATED ON)
set(CMAKE_RC_COMPILER_INIT windres)
set(CMAKE_RC_COMPILE_OBJECT "<CMAKE_RC_COMPILER> <FLAGS> -O coff <DEFINES> -i <SOURCE> -o <OBJECT>")
enable_language(RC)

View File

@ -0,0 +1,75 @@
#include <boost/process/args.hpp>
#include <boost/process/child.hpp>
#include <boost/process/exe.hpp>
#include <boost/process/group.hpp>
#include <boost/process/io.hpp>
#include <tchar.h>
#include <windows.h>
#include <cstdlib>
#include <cwchar>
#include <iostream>
#include <system_error>
#include <string_view>
static bool enableVirtualTerminalProcessing(DWORD nStdHandle)
{
auto stdHandle = GetStdHandle(nStdHandle);
if (stdHandle == INVALID_HANDLE_VALUE) {
return false;
}
auto dwMode = DWORD();
if (!GetConsoleMode(stdHandle, &dwMode)) {
return false;
}
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
return SetConsoleMode(stdHandle, dwMode);
}
int wmain(int argc, wchar_t *argv[])
{
namespace bp = boost::process;
// setup console
SetConsoleCP(CP_UTF8);
SetConsoleOutputCP(CP_UTF8);
if (enableVirtualTerminalProcessing(STD_OUTPUT_HANDLE) && enableVirtualTerminalProcessing(STD_ERROR_HANDLE)) {
SetEnvironmentVariable(L"ENABLE_ESCAPE_CODES", L"1");
}
// determine the wrapper executable path
wchar_t pathBuffer[MAX_PATH];
if (!GetModuleFileNameW(nullptr, pathBuffer, MAX_PATH)) {
std::cerr << "Unable to determine wrapper executable path: " << std::error_code(GetLastError(), std::system_category()) << '\n';
return EXIT_FAILURE;
}
// replace "-cli.exe" with just ".exe" to determine the main executable path
const auto path = std::wstring_view(pathBuffer);
auto filenameStart = path.rfind(L'\\');
if (filenameStart == std::wstring_view::npos) {
filenameStart = 0;
}
const auto appendixStart = std::wstring_view(pathBuffer + filenameStart, path.size() - filenameStart).rfind(L"-cli.exe");
if (appendixStart == std::wstring_view::npos) {
std::cerr << "Unable to determine main executable path: unexpected wrapper executable name\n";
return EXIT_FAILURE;
}
std::wcscpy(pathBuffer + filenameStart + appendixStart, L".exe");
// launch main executable via group and wait for its termination
auto ec = std::error_code();
auto group = bp::group();
auto child = bp::child(bp::exe(pathBuffer), bp::args(++argv), bp::std_out > stdout, bp::std_err > stderr, bp::std_in < stdin, ec);
if (ec) {
std::cerr << "Unable to launch \"" << argv[0] << "\": " << ec.message() << '\n';
return EXIT_FAILURE;
}
group.wait(ec);
if (ec) {
std::cerr << "Unable to wait for group to terminate: " << ec.message() << '\n';
return EXIT_FAILURE;
}
return child.exit_code();
}

View File

@ -0,0 +1,38 @@
# if defined(UNDER_CE)
# include <winbase.h>
# else
# include <windows.h>
# endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION @META_VERSION_MAJOR@,@META_VERSION_MINOR@,@META_VERSION_PATCH@,0
PRODUCTVERSION @META_VERSION_MAJOR@,@META_VERSION_MINOR@,@META_VERSION_PATCH@,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS__WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "@META_APP_AUTHOR@\0"
VALUE "FileDescription", "@META_APP_DESCRIPTION@ - CLI wrapper\0"
VALUE "FileVersion", "@META_APP_VERSION@\0"
VALUE "LegalCopyright", "by @META_APP_AUTHOR@\0"
VALUE "OriginalFilename", "$<TARGET_FILE_NAME:@TARGET_PREFIX@@META_PROJECT_NAME@@TARGET_SUFFIX@-cli>\0"
VALUE "ProductName", "@META_APP_NAME@\0"
VALUE "ProductVersion", "@META_APP_VERSION@\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0409, 1200
END
END
/* End of Version info */