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. Unfortunately this does not mean the existing hacks can be removed. Without them the wrapper still doesn't get any I/O from the GUI application.
This commit is contained in:
parent
caace3ac63
commit
f3cb406ebe
|
@ -98,9 +98,16 @@ set(CMAKE_TEMPLATE_FILES
|
||||||
cmake/templates/template.pc.in)
|
cmake/templates/template.pc.in)
|
||||||
set(SCRIPT_FILES)
|
set(SCRIPT_FILES)
|
||||||
if (MINGW)
|
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)
|
list(APPEND SCRIPT_FILES scripts/wine.sh)
|
||||||
endif ()
|
endif ()
|
||||||
|
if (WIN32)
|
||||||
|
list(APPEND CMAKE_TEMPLATE_FILES cmake/templates/cli-wrapper.cpp)
|
||||||
|
endif ()
|
||||||
|
set(EXCLUDED_FILES cmake/templates/cli-wrapper.cpp)
|
||||||
|
|
||||||
set(DOC_FILES README.md doc/buildvariables.md doc/testapplication.md)
|
set(DOC_FILES README.md doc/buildvariables.md doc/testapplication.md)
|
||||||
set(EXTRA_FILES tests/calculateoverallcoverage.awk coding-style.clang-format)
|
set(EXTRA_FILES tests/calculateoverallcoverage.awk coding-style.clang-format)
|
||||||
|
|
|
@ -110,6 +110,19 @@ if (GUI_TYPE STREQUAL "MACOSX_BUNDLE")
|
||||||
endif ()
|
endif ()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
# create CLI-wrapper to be able to use CLI in Windows-termial without hacks
|
||||||
|
if (BUILD_CLI_WRAPPER)
|
||||||
|
# find source file
|
||||||
|
include(TemplateFinder)
|
||||||
|
find_template_file_full_name("cli-wrapper.cpp" CPP_UTILITIES CLI_WRAPPER_SRC_FILE)
|
||||||
|
|
||||||
|
# add and configure additional executable
|
||||||
|
set(CLI_WRAPPER_TARGET_NAME "${META_TARGET_NAME}-cli")
|
||||||
|
add_executable(${CLI_WRAPPER_TARGET_NAME} ${CLI_WRAPPER_RES_FILES} ${CLI_WRAPPER_SRC_FILE})
|
||||||
|
set_target_properties(${CLI_WRAPPER_TARGET_NAME} PROPERTIES CXX_STANDARD 17)
|
||||||
|
target_compile_definitions(${CLI_WRAPPER_TARGET_NAME} PRIVATE _CRT_SECURE_NO_WARNINGS=1)
|
||||||
|
endif ()
|
||||||
|
|
||||||
# add install targets
|
# add install targets
|
||||||
if (NOT META_NO_INSTALL_TARGETS AND ENABLE_INSTALL_TARGETS)
|
if (NOT META_NO_INSTALL_TARGETS AND ENABLE_INSTALL_TARGETS)
|
||||||
# add install target for binary
|
# add install target for binary
|
||||||
|
@ -129,6 +142,9 @@ if (NOT META_NO_INSTALL_TARGETS AND ENABLE_INSTALL_TARGETS)
|
||||||
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}${SELECTED_LIB_SUFFIX}" COMPONENT binary)
|
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}${SELECTED_LIB_SUFFIX}" COMPONENT binary)
|
||||||
else ()
|
else ()
|
||||||
install(TARGETS ${META_TARGET_NAME} RUNTIME DESTINATION bin COMPONENT binary)
|
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 ()
|
endif ()
|
||||||
|
|
||||||
if (NOT TARGET install-binary)
|
if (NOT TARGET install-binary)
|
||||||
|
|
|
@ -6,23 +6,28 @@ if (DEFINED TEMPLATE_FINDER_LOADED)
|
||||||
endif ()
|
endif ()
|
||||||
set(TEMPLATE_FINDER_LOADED YES)
|
set(TEMPLATE_FINDER_LOADED YES)
|
||||||
|
|
||||||
function (find_template_file FILE_NAME PROJECT_VAR_NAME OUTPUT_VAR)
|
function (find_template_file FILE_NAME_WITHOUT_EXTENSION PROJECT_VAR_NAME OUTPUT_VAR)
|
||||||
if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${FILE_NAME}.in")
|
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
|
# check own source directory
|
||||||
set(${OUTPUT_VAR}
|
set(${OUTPUT_VAR}
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${FILE_NAME}.in"
|
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${FILE_NAME}"
|
||||||
PARENT_SCOPE)
|
PARENT_SCOPE)
|
||||||
message(STATUS "Using template for ${FILE_NAME} from own (${META_PROJECT_NAME}) source directory.")
|
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
|
# check sources of project
|
||||||
set(${OUTPUT_VAR}
|
set(${OUTPUT_VAR}
|
||||||
"${${PROJECT_VAR_NAME}_SOURCE_DIR}/cmake/templates/${FILE_NAME}.in"
|
"${${PROJECT_VAR_NAME}_SOURCE_DIR}/cmake/templates/${FILE_NAME}"
|
||||||
PARENT_SCOPE)
|
PARENT_SCOPE)
|
||||||
message(STATUS "Using template for ${FILE_NAME} from ${PROJECT_VAR_NAME} source directory.")
|
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
|
# check installed version of project
|
||||||
set(${OUTPUT_VAR}
|
set(${OUTPUT_VAR}
|
||||||
"${${PROJECT_VAR_NAME}_CONFIG_DIRS}/templates/${FILE_NAME}.in"
|
"${${PROJECT_VAR_NAME}_CONFIG_DIRS}/templates/${FILE_NAME}"
|
||||||
PARENT_SCOPE)
|
PARENT_SCOPE)
|
||||||
message(STATUS "Using template for ${FILE_NAME} from ${PROJECT_VAR_NAME} installation.")
|
message(STATUS "Using template for ${FILE_NAME} from ${PROJECT_VAR_NAME} installation.")
|
||||||
else ()
|
else ()
|
||||||
|
|
|
@ -19,8 +19,9 @@ endif ()
|
||||||
# find rc template, define path of output rc file
|
# find rc template, define path of output rc file
|
||||||
include(TemplateFinder)
|
include(TemplateFinder)
|
||||||
find_template_file("windows.rc" CPP_UTILITIES RC_TEMPLATE_FILE)
|
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_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
|
# create Windows icon from png with ffmpeg if available
|
||||||
unset(WINDOWS_ICON_RC_ENTRY)
|
unset(WINDOWS_ICON_RC_ENTRY)
|
||||||
|
@ -51,10 +52,21 @@ file(
|
||||||
GENERATE
|
GENERATE
|
||||||
OUTPUT "${WINDOWS_RC_FILE}-$<CONFIG>.rc"
|
OUTPUT "${WINDOWS_RC_FILE}-$<CONFIG>.rc"
|
||||||
INPUT "${WINDOWS_RC_FILE}-configured.rc")
|
INPUT "${WINDOWS_RC_FILE}-configured.rc")
|
||||||
|
if (BUILD_CLI_WRAPPER AND META_PROJECT_TYPE STREQUAL "application")
|
||||||
|
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")
|
||||||
|
endif ()
|
||||||
|
|
||||||
# set windres as resource compiler
|
# set windres as resource compiler
|
||||||
list(APPEND RES_FILES "${WINDOWS_RC_FILE}-${CMAKE_BUILD_TYPE}.rc")
|
list(APPEND RES_FILES "${WINDOWS_RC_FILE}-${CMAKE_BUILD_TYPE}.rc")
|
||||||
set_property(SOURCE "${WINDOWS_RC_FILE}-${CMAKE_BUILD_TYPE}.rc" PROPERTY GENERATED ON)
|
set_property(SOURCE "${WINDOWS_RC_FILE}-${CMAKE_BUILD_TYPE}.rc" PROPERTY GENERATED ON)
|
||||||
|
if (BUILD_CLI_WRAPPER AND META_PROJECT_TYPE STREQUAL "application")
|
||||||
|
list(APPEND CLI_WRAPPER_RES_FILES "${WINDOWS_CLI_RC_FILE}-${CMAKE_BUILD_TYPE}.rc")
|
||||||
|
set_property(SOURCE "${WINDOWS_CLI_RC_FILE}-${CMAKE_BUILD_TYPE}.rc" PROPERTY GENERATED ON)
|
||||||
|
endif ()
|
||||||
set(CMAKE_RC_COMPILER_INIT windres)
|
set(CMAKE_RC_COMPILER_INIT windres)
|
||||||
set(CMAKE_RC_COMPILE_OBJECT "<CMAKE_RC_COMPILER> <FLAGS> -O coff <DEFINES> -i <SOURCE> -o <OBJECT>")
|
set(CMAKE_RC_COMPILE_OBJECT "<CMAKE_RC_COMPILER> <FLAGS> -O coff <DEFINES> -i <SOURCE> -o <OBJECT>")
|
||||||
enable_language(RC)
|
enable_language(RC)
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cwchar>
|
||||||
|
#include <iostream>
|
||||||
|
#include <system_error>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Enables virutal terminal processing.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Returns \a replacement if \a value matches \a key; otherwise returns \a value.
|
||||||
|
*/
|
||||||
|
static std::size_t replace(std::size_t value, std::size_t key, std::size_t replacement)
|
||||||
|
{
|
||||||
|
return value == key ? replacement : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Sets the console up and launches the "main" application.
|
||||||
|
*/
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
// setup console
|
||||||
|
// -> enable UTF-8 as this is used by all my applications
|
||||||
|
SetConsoleCP(CP_UTF8);
|
||||||
|
SetConsoleOutputCP(CP_UTF8);
|
||||||
|
// -> ensure environment variables for hack to attach to parent's console are enabled; this is still required
|
||||||
|
// for this wrapper to receive standard I/O
|
||||||
|
SetEnvironmentVariableW(L"ENABLE_CONSOLE", L"1");
|
||||||
|
SetEnvironmentVariableW(L"ENABLE_CP_UTF8", L"1");
|
||||||
|
// -> unset environment variables that would lead to skipping the hack; for the wrapper the hack is even
|
||||||
|
// required when using Mintty
|
||||||
|
SetEnvironmentVariableW(L"MSYSCON", L"");
|
||||||
|
SetEnvironmentVariableW(L"TERM_PROGRAM", L"");
|
||||||
|
// -> enable support for ANSI escape codes if possible
|
||||||
|
if (enableVirtualTerminalProcessing(STD_OUTPUT_HANDLE) && enableVirtualTerminalProcessing(STD_ERROR_HANDLE)) {
|
||||||
|
SetEnvironmentVariableW(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" in the wrapper executable's file name with just ".exe" to make up the main executable path
|
||||||
|
const auto path = std::wstring_view(pathBuffer);
|
||||||
|
const auto filenameStart = replace(path.rfind(L'\\'), std::wstring_view::npos, 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");
|
||||||
|
|
||||||
|
// compute startup parameters
|
||||||
|
auto commadLine = GetCommandLineW();
|
||||||
|
auto processInformation = PROCESS_INFORMATION();
|
||||||
|
auto startupInfo = STARTUPINFOW();
|
||||||
|
ZeroMemory(&startupInfo, sizeof(startupInfo));
|
||||||
|
ZeroMemory(&processInformation, sizeof(processInformation));
|
||||||
|
startupInfo.cb = sizeof(startupInfo);
|
||||||
|
startupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||||
|
startupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
||||||
|
startupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
||||||
|
startupInfo.dwFlags |= STARTF_USESTDHANDLES;
|
||||||
|
|
||||||
|
// start main executable in new group and print debug information if that's not possible
|
||||||
|
auto res = CreateProcessW(
|
||||||
|
pathBuffer, // path of main executable
|
||||||
|
commadLine, // command line arguments
|
||||||
|
nullptr, // process handle not inheritable
|
||||||
|
nullptr, // thread handle not inheritable
|
||||||
|
true, // set handle inheritance to true
|
||||||
|
CREATE_NEW_PROCESS_GROUP, // creation flags
|
||||||
|
nullptr, // use parent's environment block
|
||||||
|
nullptr, // use parent's starting directory
|
||||||
|
&startupInfo, // pointer to STARTUPINFO structure
|
||||||
|
&processInformation); // pointer to PROCESS_INFORMATION structure
|
||||||
|
if (!res) {
|
||||||
|
std::cerr << "Unable to launch main executable: " << std::error_code(GetLastError(), std::system_category()) << '\n';
|
||||||
|
std::wcerr << L" - assumed path: " << pathBuffer << L'\n';
|
||||||
|
std::wcerr << L" - assumed command-line: " << commadLine << L'\n';
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for main executable and possible children to terminate and return exit code
|
||||||
|
auto exitCode = DWORD();
|
||||||
|
WaitForSingleObject(processInformation.hProcess, INFINITE);
|
||||||
|
GetExitCodeProcess(processInformation.hProcess, &exitCode);
|
||||||
|
return exitCode;
|
||||||
|
}
|
|
@ -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 */
|
Loading…
Reference in New Issue