From 451068c4bd8a25ea69bbf93cc33fd5f356b75e72 Mon Sep 17 00:00:00 2001 From: timbos Date: Sat, 7 Nov 2026 13:24:14 +0100 Subject: [PATCH 1/3] Packaging: fix install rules, use system RtMidi via pkg-config, CPack component Hi Martin, Long time no speak. Hope things are going well with you and your family. I started to prepare to build a preset for the Korg Z1 and added package generation directives (cpack). On my machine I created Deb, tgz, rpm, (windows .zip and .exe ) packages and strived to make it as portable as possible. On Debian the build and deployment was tested and on Windows the .zip and .exe where tested. Maybe after some test you can add the packages to the git repo release section or on your site. Best regards, Tim (Prague) ## Summary - Fix `cpack` failure (installed header from non-existent `src/sysex.hpp`), remove duplicate install rules from CMakeLists.txt - Use distro RtMidi via pkg-config when no CMake config exists (Debian/Ubuntu `librtmidi-dev`) - Package only project files (CPack component); FetchContent deps no longer leak RtMidi/ArduinoJson/doctest headers into the package - DEB/RPM declare RtMidi as dependency - Add MinGW-w64 toolchain to cross-compile Windows ZIP/NSIS packages on Linux - Silence GCC 14 false-positive -Wstringop-overflow in tests - README.md: includes the packaging instructions ## Tests - Built of all packages: Operating System: Debian GNU/Linux 13 (trixie) Linux 6.16.3+deb13-amd64 Architecture: x86-64 - Deployed Debian: DEB next to librtmidi-dev, contains only electraone files - Windows cross build: electraone.exe depends only on KERNEL32/msvcrt/WINMM - Tested and deployed the .zip and .exe installer on Win10. - During tests there was 1 Warning during the test, "writing 1 byte into a region of size 0", with a follow-up note about a 13-byte buffer I added -Wno-stringop-overflow for the test target with GCC only (GCC 14) --- CMakeLists.txt | 105 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 201a697..1d5cca6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,12 +7,24 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() +if(MINGW) + # Bundle libstdc++, libgcc and winpthread so the .exe runs without MinGW DLLs. + add_link_options(-static) +endif() + include(FetchContent) # ---- RtMidi --------------------------------------------------------------- find_package(RtMidi CONFIG QUIET) if(NOT RtMidi_FOUND) - message(STATUS "RtMidi not found via find_package, fetching from source") + # Debian/Ubuntu librtmidi-dev ships only a pkg-config file, no CMake config. + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(RTMIDI_PC IMPORTED_TARGET rtmidi) + endif() +endif() +if(NOT RtMidi_FOUND AND NOT RTMIDI_PC_FOUND) + message(STATUS "RtMidi not found via find_package or pkg-config, fetching from source") set(RTMIDI_BUILD_TESTING OFF CACHE BOOL "" FORCE) # Static, not RtMidi's own default (shared): keeps electraone/electraone_api # self-contained everywhere, instead of needing librtmidi.so/.dylib/.dll @@ -52,8 +64,14 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(ArduinoJson) -# Resolve once: find_package gives a namespaced target, FetchContent gives a bare one. -set(RTMIDI_TARGET $,RtMidi::rtmidi,rtmidi>) +# Resolve once: find_package gives a namespaced target, pkg-config an imported one, FetchContent a bare one. +if(RtMidi_FOUND) + set(RTMIDI_TARGET RtMidi::rtmidi) +elseif(RTMIDI_PC_FOUND) + set(RTMIDI_TARGET PkgConfig::RTMIDI_PC) +else() + set(RTMIDI_TARGET rtmidi) +endif() set(CLI11_TARGET $,CLI11::CLI11,CLI11>) # ---- electraone API library ------------------------------------------------ @@ -93,13 +111,6 @@ target_link_libraries(electraone_api PRIVATE ArduinoJson ) -install(TARGETS electraone_api ARCHIVE DESTINATION lib) -install(FILES - include/electraone/ElectraOneClient.hpp - src/sysex.hpp - DESTINATION include/electraone -) - # ---- electraone CLI ------------------------------------------------------------ # Single source of truth for the CLI's displayed/binary name, propagated into # the app via the generated electraone_version.hpp header below. @@ -149,8 +160,6 @@ target_link_libraries(${ELECTRAONE_CLI_NAME} PRIVATE ArduinoJson ) -install(TARGETS ${ELECTRAONE_CLI_NAME} RUNTIME DESTINATION bin) - # ---- examples --------------------------------------------------------------- # Built the same way on every platform via `cmake --build build` - no manual # per-OS link line needed, since electraone_api already carries its RtMidi @@ -170,7 +179,9 @@ endif() # the top-level CMake project, so embedding electraone_api via # add_subdirectory in another project doesn't pull in our test target too. option(ELECTRAONE_BUILD_TESTS "Build unit tests" ON) -if(ELECTRAONE_BUILD_TESTS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) +# doctest_discover_tests runs the test binary at build time, which needs an emulator when cross-compiling. +if(ELECTRAONE_BUILD_TESTS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR + AND (NOT CMAKE_CROSSCOMPILING OR CMAKE_CROSSCOMPILING_EMULATOR)) FetchContent_Declare( doctest GIT_REPOSITORY https://github.com/doctest/doctest.git @@ -190,7 +201,75 @@ if(ELECTRAONE_BUILD_TESTS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR ) target_include_directories(electraone_tests PRIVATE src) target_link_libraries(electraone_tests PRIVATE electraone_api doctest::doctest) + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + # GCC 14 false positive on vector insert + push_back in test_sysex.cpp. + target_compile_options(electraone_tests PRIVATE -Wno-stringop-overflow) + endif() include(${doctest_SOURCE_DIR}/scripts/cmake/doctest.cmake) doctest_discover_tests(electraone_tests) endif() + +# ========================================== +# 1. INSTALLATION DIRECTIVES +# ========================================== +include(GNUInstallDirs) + +install(TARGETS ${ELECTRAONE_CLI_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT electraone) +install(TARGETS electraone_api ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT electraone) +if(RTMIDI_TARGET STREQUAL "rtmidi") + # No system RtMidi to depend on (e.g. Windows): ship the fetched static lib for libelectraone.a users. + install(FILES $ DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT electraone) +endif() +install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/include/electraone/ElectraOneClient.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/electraone/sysex.hpp + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/electraone + COMPONENT electraone +) + +# ========================================== +# CPACK CONFIGURATION +# ========================================== +set(CPACK_PACKAGE_NAME "electraone") +set(CPACK_PACKAGE_VENDOR "Electra One") +set(CPACK_PACKAGE_VERSION "1.0") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A cross-platform command-line suite and C++ client library for the Electra One SysEx protocol.") +set(CPACK_PACKAGE_CONTACT "info@electra.one") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") + +# Package only our component; FetchContent deps (RtMidi, ArduinoJson, doctest) add their own install rules. +set(CPACK_INSTALL_CMAKE_PROJECTS "${CMAKE_BINARY_DIR};${PROJECT_NAME};electraone;/") + +if(WIN32) + # ZIP always; NSIS installer when makensis is available (apt install nsis). + set(CPACK_GENERATOR "ZIP") + find_program(MAKENSIS_EXECUTABLE makensis) + if(MAKENSIS_EXECUTABLE) + list(APPEND CPACK_GENERATOR "NSIS") + endif() + set(CPACK_PACKAGE_INSTALL_DIRECTORY "electraone") + set(CPACK_NSIS_DISPLAY_NAME "electraone") + set(CPACK_NSIS_MODIFY_PATH ON) + set(CPACK_NSIS_URL_INFO_ABOUT "https://electra.one") +else() + set(CPACK_GENERATOR "RPM;DEB;TGZ") +endif() + +# Debian Target Configuration +set(CPACK_DEBIAN_PACKAGE_SECTION "sound") +set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") +set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://electra.one") +set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) +# libelectraone.a consumers need the RtMidi headers and library too. +set(CPACK_DEBIAN_PACKAGE_DEPENDS "librtmidi-dev") + +# RPM Target Configuration (Fedora/RHEL/openSUSE) +set(CPACK_RPM_PACKAGE_LICENSE "GPLv3") +set(CPACK_RPM_PACKAGE_GROUP "Applications/Multimedia") +set(CPACK_RPM_PACKAGE_RELEASE "1") +set(CPACK_RPM_PACKAGE_REQUIRES "libjack.so.0()(64bit), libasound.so.2()(64bit), rtmidi-devel") +set(CPACK_RPM_PACKAGE_URL "https://electra.one") + +include(CPack) + From ca88dbf2d1d38a98798ed7489357fb728b48dcd3 Mon Sep 17 00:00:00 2001 From: timbos Date: Sat, 7 Nov 2026 13:24:42 +0100 Subject: [PATCH 2/3] Add MinGW-w64 toolchain for cross-compiling Windows packages --- .gitignore | 3 ++- cmake/toolchains/mingw-w64-x86_64.cmake | 27 +++++++++++++++++++++++++ src/commands/terminal.cpp | 2 ++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 cmake/toolchains/mingw-w64-x86_64.cmake diff --git a/.gitignore b/.gitignore index e227a8b..61ba0e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build/ .claude -basic_usage \ No newline at end of file +basic_usage +build-*/ diff --git a/cmake/toolchains/mingw-w64-x86_64.cmake b/cmake/toolchains/mingw-w64-x86_64.cmake new file mode 100644 index 0000000..4509e33 --- /dev/null +++ b/cmake/toolchains/mingw-w64-x86_64.cmake @@ -0,0 +1,27 @@ +# Cross-compile for 64-bit Windows with MinGW-w64 (Debian/Ubuntu: apt install mingw-w64). +# cmake -S . -B build-win64 -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64-x86_64.cmake -DCMAKE_BUILD_TYPE=Release +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +set(MINGW_TRIPLET x86_64-w64-mingw32) + +# The -posix variants provide std::thread (the -win32 ones do not). +find_program(CMAKE_C_COMPILER NAMES ${MINGW_TRIPLET}-gcc-posix ${MINGW_TRIPLET}-gcc REQUIRED) +find_program(CMAKE_CXX_COMPILER NAMES ${MINGW_TRIPLET}-g++-posix ${MINGW_TRIPLET}-g++ REQUIRED) +find_program(CMAKE_RC_COMPILER NAMES ${MINGW_TRIPLET}-windres REQUIRED) + +set(CMAKE_FIND_ROOT_PATH /usr/${MINGW_TRIPLET}) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + +# Keep pkg-config from returning the host's Linux packages (e.g. rtmidi.pc). +set(ENV{PKG_CONFIG_LIBDIR} /usr/${MINGW_TRIPLET}/lib/pkgconfig) +set(ENV{PKG_CONFIG_PATH} "") + +# Run cross-built test binaries through wine when it is installed. +find_program(WINE_EXECUTABLE wine) +if(WINE_EXECUTABLE) + set(CMAKE_CROSSCOMPILING_EMULATOR ${WINE_EXECUTABLE}) +endif() diff --git a/src/commands/terminal.cpp b/src/commands/terminal.cpp index 7f4989c..4303ce5 100644 --- a/src/commands/terminal.cpp +++ b/src/commands/terminal.cpp @@ -24,7 +24,9 @@ #include #ifdef _WIN32 +#ifndef NOMINMAX #define NOMINMAX +#endif #include #include #else From ad05ba79d9089e142dacea7aa117bb5ce761b6d7 Mon Sep 17 00:00:00 2001 From: timbos Date: Sat, 7 Nov 2026 13:25:34 +0100 Subject: [PATCH 3/3] Document building DEB/RPM/TGZ and Windows .zip and .exe installer packages --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/README.md b/README.md index 0392dbe..19269e7 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,81 @@ This produces: Exact output paths / names differ slightly per platform. +### Packages (DEB, RPM, TGZ) + +After building, create packages with CPack from the `build/` directory: + +```bash +cd build +cpack -G DEB # build/electraone-1.0-Linux.deb +cpack -G RPM # build/electraone-1.0-Linux.rpm +cpack -G TGZ # build/electraone-1.0-Linux.tar.gz +``` + +Running `cpack` without `-G` builds all three. Packaging needs: + +- DEB: `dpkg-dev` (for `dpkg-shlibdeps`) +- RPM: `rpmbuild`, on Debian/Ubuntu provided by `sudo apt install rpm` + +The package contains only `electraone`, `libelectraone.a` and the +`electraone/*.hpp` headers. RtMidi is a package dependency (`librtmidi-dev` / +`rtmidi-devel`), so install the system RtMidi before configuring and use +`apt`/`dnf` to install the package, which resolves dependencies: + +```bash +sudo apt install ./build/electraone-1.0-Linux.deb +sudo dnf install ./build/electraone-1.0-Linux.rpm +``` + +#### Windows packages, cross-compiled on Linux + +The Windows `.zip` and installer `.exe` can be built on Debian/Ubuntu with +MinGW-w64, no Windows machine needed. + +Required packages: + +| Package | Needed for | +|---|---| +| `cmake`, `git` | configure, fetching RtMidi/CLI11/ArduinoJson | +| `mingw-w64` | cross compiler (pulls in `g++-mingw-w64-x86-64-posix` and `binutils-mingw-w64-x86-64`) | +| `nsis` | optional, the `makensis` installer builder for `cpack -G NSIS` | +| `wine` | optional, runs the unit tests on the build host | + +```bash +sudo apt install cmake git mingw-w64 # required +sudo apt install nsis wine # optional +``` + +No Windows libraries need to be installed: RtMidi is fetched and built +statically against WinMM, and ALSA/JACK or the host `librtmidi-dev` are not +used. The toolchain file selects the `-posix` MinGW compiler, which is +required because the code uses `std::thread`. + +Build and package in a separate build directory: + +```bash +cmake -S . -B build-win64 -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64-x86_64.cmake -DCMAKE_BUILD_TYPE=Release +cmake --build build-win64 +cd build-win64 +cpack -G ZIP # electraone-1.0-win64.zip +cpack -G NSIS # electraone-1.0-win64.exe (installer, needs nsis) +``` + +Running `cpack` without `-G` builds the ZIP, plus the NSIS installer when +`makensis` is found. Reconfigure (`cmake .` in `build-win64/`) after +installing `nsis` or `wine`, since both are detected at configure time. + +Package contents: + +- `bin/electraone.exe` - MinGW runtime linked statically, needs only system + DLLs (`KERNEL32`, `msvcrt`, `WINMM`) +- `lib/libelectraone.a`, `lib/librtmidi.a` - for programs linking the C++ + API library with MinGW (also link `-lwinmm`) +- `include/electraone/*.hpp` + +The installer installs to `C:\Program Files\electraone` and offers to add +`bin` to `PATH`. Unit tests are skipped unless `wine` is installed. + ### Prerequisites per platform #### Linux