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/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) + 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 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