Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
build/
.claude
basic_usage
basic_usage
build-*/
105 changes: 92 additions & 13 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 $<IF:$<TARGET_EXISTS:RtMidi::rtmidi>,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 $<IF:$<TARGET_EXISTS:CLI11::CLI11>,CLI11::CLI11,CLI11>)

# ---- electraone API library ------------------------------------------------
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 $<TARGET_LINKER_FILE:rtmidi> 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)

75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions cmake/toolchains/mingw-w64-x86_64.cmake
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 2 additions & 0 deletions src/commands/terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
#include <cstdio>

#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <io.h>
#include <windows.h>
#else
Expand Down