diff --git a/.github/workflows/build_python.yml b/.github/workflows/build_python.yml new file mode 100644 index 00000000..53fc7624 --- /dev/null +++ b/.github/workflows/build_python.yml @@ -0,0 +1,113 @@ +name: Python + +on: + push: + paths-ignore: + - 'docs/**' + - '**.md' + pull_request: + paths-ignore: + - '**.md' + - 'docs/**' + +jobs: + build: + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + strategy: + fail-fast: false + matrix: + header_only: [0, 1] + config: + - { + name: "Windows (MSVC)", + os: windows-latest, + generator: "", + cmakeflags: "-DLIBREMIDI_NO_WINUWP=0 -DBOOST_ROOT=$PWD/boost_1_86_0 -DCMAKE_GENERATOR_PLATFORM=version=10.0.22621.0", + environment: "" + } + - { + name: "Ubuntu (gcc)", + os: ubuntu-latest, + generator: "", + cmakeflags: "-DCMAKE_CXX_FLAGS='-Werror=return-type -fsanitize=address -fsanitize=undefined -D_GLIBCXX_DEBUG=1 -D_GLIBCXX_DEBUG_PEDANTIC=1 -D_GLIBCXX_ASSERTIONS=1 -D_GLIBCXX_SANITIZE_VECTOR=1'", + environment: "LD_PRELOAD=/usr/lib/gcc/x86_64-linux-gnu/13/libasan.so:/usr/lib/gcc/x86_64-linux-gnu/13/libubsan.so" + } + - { + name: "Ubuntu (clang, libstdc++)", + os: ubuntu-latest, + generator: "", + cmakeflags: "-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS='-Werror=return-type'", + environment: "" + } + - { + name: "Ubuntu (clang, libc++)", + os: ubuntu-latest, + generator: "", + cmakeflags: "-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS='-stdlib=libc++ -Werror=return-type'", + environment: "" + } + - { + name: "macOS", + os: macos-14, + generator: "", + cmakeflags: "-DCMAKE_CXX_FLAGS=-Werror=return-type -DBOOST_ROOT=$PWD/boost_1_86_0", + environment: "" + } + + steps: + - uses: actions/checkout@v4 + + - name: Get latest release version number + id: get_version + uses: dhkatz/get-version-action@main + + - uses: maxim-lobanov/setup-xcode@v1 + if: runner.os == 'macOS' + with: + xcode-version: latest-stable + + - name: Install dependencies + run: | + if [ "$RUNNER_OS" == "Linux" ]; then + sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" + sudo apt update + sudo apt install cmake libboost-dev libasound-dev libjack-jackd2-dev clang libc++-dev + else + curl -L https://github.com/ossia/sdk/releases/download/sdk31/boost_1_86_0.tar.gz > boost.tar.gz + tar -xzf boost.tar.gz + rm boost.tar.gz + fi + shell: bash + + - name: Configure + shell: bash + run: | + cmake -S ./bindings/python -B build \ + ${{ matrix.config.generator }} \ + ${{ matrix.config.cmakeflags }} \ + -DCMAKE_BUILD_TYPE=Debug \ + -DLIBREMIDI_FIND_BOOST=1 \ + -DLIBREMIDI_HEADER_ONLY=${{ matrix.header_only }} \ + -DLIBREMIDI_CI=1 + + - name: Build + run: | + cmake --build build --config Debug + + - name: Test + if: runner.os == 'Windows' + shell: bash + run: | + export PYTHONPATH=$PWD/build/Debug + find . -name '*.pyd' + # ${{matrix.config.environment}} python tests/python/list_apis.py + + - name: Test + if: runner.os != 'Windows' + shell: bash + run: | + export PYTHONPATH=$PWD/build + export ASAN_OPTIONS=detect_leaks=0 + ${{matrix.config.environment}} python tests/python/list_apis.py + diff --git a/bindings/python/CMakeLists.txt b/bindings/python/CMakeLists.txt index ad5f2f95..1faf865f 100644 --- a/bindings/python/CMakeLists.txt +++ b/bindings/python/CMakeLists.txt @@ -3,6 +3,7 @@ project(pylibremidi) set(LIBREMIDI_HEADER_ONLY 1) set(LIBREMIDI_NEEDS_READERWRITERQUEUE 1) +set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL 1) add_subdirectory(../.. libremidi-src) find_package(Python 3.8 COMPONENTS Interpreter Development.Module REQUIRED) @@ -14,4 +15,4 @@ FetchContent_Declare( FetchContent_MakeAvailable(nanobind) nanobind_add_module(pylibremidi pylibremidi.cpp) -target_link_libraries(pylibremidi PUBLIC libremidi) +target_link_libraries(pylibremidi PUBLIC libremidi readerwriterqueue) diff --git a/bindings/python/test.py b/bindings/python/example.py similarity index 96% rename from bindings/python/test.py rename to bindings/python/example.py index b95bcc08..06a76d1b 100644 --- a/bindings/python/test.py +++ b/bindings/python/example.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3.13 +#!/usr/bin/env python import pylibremidi as lm observer_config = lm.ObserverConfiguration() observer = lm.Observer(observer_config, lm.midi2_default_api()) diff --git a/bindings/python/pylibremidi.cpp b/bindings/python/pylibremidi.cpp index 2d6799be..f50c6f23 100644 --- a/bindings/python/pylibremidi.cpp +++ b/bindings/python/pylibremidi.cpp @@ -49,10 +49,10 @@ using midi_out_msg = boost::variant2::variant; struct observer_poll_wrapper { moodycamel::ReaderWriterQueue queue{}; observer_configuration conf; - observer observer; - explicit observer_poll_wrapper(observer_configuration conf = {}) noexcept : conf{conf}, observer{this->process(std::move(conf))} {} + observer impl; + explicit observer_poll_wrapper(observer_configuration conf = {}) noexcept : conf{conf}, impl{this->process(std::move(conf))} {} - explicit observer_poll_wrapper(observer_configuration conf, libremidi::observer_api_configuration api_conf) : conf{conf}, observer{process(std::move(conf)), std::move(api_conf)} {} + explicit observer_poll_wrapper(observer_configuration conf, libremidi::observer_api_configuration api_conf) : conf{conf}, impl{process(std::move(conf)), std::move(api_conf)} {} observer_configuration process(observer_configuration &&obs) { if (obs.on_error) @@ -91,12 +91,12 @@ struct midi_in_poll_wrapper { input_configuration orig_callbacks; ump_input_configuration ump_callbacks; - midi_in midi_in; + midi_in impl; - explicit midi_in_poll_wrapper(const input_configuration &conf) noexcept : orig_callbacks{conf}, midi_in{this->process(std::move(conf))} {} - explicit midi_in_poll_wrapper(input_configuration conf, input_api_configuration api_conf) : orig_callbacks{conf}, midi_in{this->process(std::move(conf)), std::move(api_conf)} {} - explicit midi_in_poll_wrapper(ump_input_configuration conf) noexcept : ump_callbacks{conf}, midi_in{this->process(std::move(conf))} {} - explicit midi_in_poll_wrapper(ump_input_configuration conf, input_api_configuration api_conf) : ump_callbacks{conf}, midi_in{this->process(std::move(conf)), std::move(api_conf)} {} + explicit midi_in_poll_wrapper(const input_configuration &conf) noexcept : orig_callbacks{conf}, impl{this->process(std::move(conf))} {} + explicit midi_in_poll_wrapper(input_configuration conf, input_api_configuration api_conf) : orig_callbacks{conf}, impl{this->process(std::move(conf)), std::move(api_conf)} {} + explicit midi_in_poll_wrapper(ump_input_configuration conf) noexcept : ump_callbacks{conf}, impl{this->process(std::move(conf))} {} + explicit midi_in_poll_wrapper(ump_input_configuration conf, input_api_configuration api_conf) : ump_callbacks{conf}, impl{this->process(std::move(conf)), std::move(api_conf)} {} input_configuration process(input_configuration obs) { orig_callbacks = obs; @@ -155,11 +155,11 @@ struct midi_in_poll_wrapper { struct midi_out_poll_wrapper { moodycamel::ReaderWriterQueue queue{}; output_configuration orig_callbacks; - midi_out midi_out; - explicit midi_out_poll_wrapper() noexcept : midi_out{} {} + midi_out impl; + explicit midi_out_poll_wrapper() noexcept : impl{} {} - explicit midi_out_poll_wrapper(const output_configuration &conf) noexcept : orig_callbacks{conf}, midi_out{this->process(std::move(conf))} {} - explicit midi_out_poll_wrapper(output_configuration conf, output_api_configuration api_conf) : orig_callbacks{conf}, midi_out{this->process(std::move(conf)), std::move(api_conf)} {} + explicit midi_out_poll_wrapper(const output_configuration &conf) noexcept : orig_callbacks{conf}, impl{this->process(std::move(conf))} {} + explicit midi_out_poll_wrapper(output_configuration conf, output_api_configuration api_conf) : orig_callbacks{conf}, impl{this->process(std::move(conf)), std::move(api_conf)} {} output_configuration process(output_configuration obs) { orig_callbacks = obs; @@ -389,9 +389,9 @@ NB_MODULE(pylibremidi, m) { .def(nb::init<>()) .def(nb::init()) .def(nb::init()) - .def("get_current_api", [](libremidi::observer_poll_wrapper &self) { return self.observer.get_current_api(); }) - .def("get_input_ports", [](libremidi::observer_poll_wrapper &self) { return self.observer.get_input_ports(); }) - .def("get_output_ports", [](libremidi::observer_poll_wrapper &self) { return self.observer.get_output_ports(); }) + .def("get_current_api", [](libremidi::observer_poll_wrapper &self) { return self.impl.get_current_api(); }) + .def("get_input_ports", [](libremidi::observer_poll_wrapper &self) { return self.impl.get_input_ports(); }) + .def("get_output_ports", [](libremidi::observer_poll_wrapper &self) { return self.impl.get_output_ports(); }) .def("poll", [](libremidi::observer_poll_wrapper &self) { return self.poll(); }); nb::class_(m, "MidiIn") @@ -399,16 +399,16 @@ NB_MODULE(pylibremidi, m) { .def(nb::init()) .def(nb::init()) .def(nb::init()) - .def("get_current_api", [](libremidi::midi_in_poll_wrapper &self) { return self.midi_in.get_current_api(); }) - .def("open_port", [](libremidi::midi_in_poll_wrapper &self, const libremidi::input_port &p) { return self.midi_in.open_port(p); }) - .def("open_port", [](libremidi::midi_in_poll_wrapper &self, const libremidi::input_port &p, std::string_view name) { return self.midi_in.open_port(p, name); }) - .def("open_virtual_port", [](libremidi::midi_in_poll_wrapper &self) { return self.midi_in.open_virtual_port(); }) - .def("open_virtual_port", [](libremidi::midi_in_poll_wrapper &self, std::string_view name) { return self.midi_in.open_virtual_port(name); }) - .def("set_port_name", [](libremidi::midi_in_poll_wrapper &self, std::string_view name) { return self.midi_in.set_port_name(name); }) - .def("close_port", [](libremidi::midi_in_poll_wrapper &self) { return self.midi_in.close_port(); }) - .def("is_port_open", [](libremidi::midi_in_poll_wrapper &self) { return self.midi_in.is_port_open(); }) - .def("is_port_connected", [](libremidi::midi_in_poll_wrapper &self) { return self.midi_in.is_port_connected(); }) - .def("absolute_timestamp", [](libremidi::midi_in_poll_wrapper &self) { return self.midi_in.absolute_timestamp(); }) + .def("get_current_api", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.get_current_api(); }) + .def("open_port", [](libremidi::midi_in_poll_wrapper &self, const libremidi::input_port &p) { return self.impl.open_port(p); }) + .def("open_port", [](libremidi::midi_in_poll_wrapper &self, const libremidi::input_port &p, std::string_view name) { return self.impl.open_port(p, name); }) + .def("open_virtual_port", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.open_virtual_port(); }) + .def("open_virtual_port", [](libremidi::midi_in_poll_wrapper &self, std::string_view name) { return self.impl.open_virtual_port(name); }) + .def("set_port_name", [](libremidi::midi_in_poll_wrapper &self, std::string_view name) { return self.impl.set_port_name(name); }) + .def("close_port", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.close_port(); }) + .def("is_port_open", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.is_port_open(); }) + .def("is_port_connected", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.is_port_connected(); }) + .def("absolute_timestamp", [](libremidi::midi_in_poll_wrapper &self) { return self.impl.absolute_timestamp(); }) .def("poll", &libremidi::midi_in_poll_wrapper::poll); nb::class_(m, "MidiOutBase"); @@ -416,36 +416,36 @@ NB_MODULE(pylibremidi, m) { .def(nb::init<>()) .def(nb::init()) .def(nb::init()) - .def("get_current_api", [](libremidi::midi_out_poll_wrapper &self) { return self.midi_out.get_current_api(); }) - .def("open_port", [](libremidi::midi_out_poll_wrapper &self, const libremidi::output_port &p) { return self.midi_out.open_port(p); }) - .def("open_port", [](libremidi::midi_out_poll_wrapper &self, const libremidi::output_port &p, std::string_view name) { return self.midi_out.open_port(p, name); }) - .def("open_virtual_port", [](libremidi::midi_out_poll_wrapper &self) { return self.midi_out.open_virtual_port(); }) - .def("open_virtual_port", [](libremidi::midi_out_poll_wrapper &self, std::string_view name) { return self.midi_out.open_virtual_port(name); }) - .def("set_port_name", [](libremidi::midi_out_poll_wrapper &self, std::string_view name) { return self.midi_out.set_port_name(name); }) - .def("close_port", [](libremidi::midi_out_poll_wrapper &self) { return self.midi_out.close_port(); }) - .def("is_port_open", [](libremidi::midi_out_poll_wrapper &self) { return self.midi_out.is_port_open(); }) - .def("is_port_connected", [](libremidi::midi_out_poll_wrapper &self) { return self.midi_out.is_port_connected(); }) - .def("absolute_timestamp", [](libremidi::midi_out_poll_wrapper &self) { return self.midi_out.current_time(); }) + .def("get_current_api", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.get_current_api(); }) + .def("open_port", [](libremidi::midi_out_poll_wrapper &self, const libremidi::output_port &p) { return self.impl.open_port(p); }) + .def("open_port", [](libremidi::midi_out_poll_wrapper &self, const libremidi::output_port &p, std::string_view name) { return self.impl.open_port(p, name); }) + .def("open_virtual_port", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.open_virtual_port(); }) + .def("open_virtual_port", [](libremidi::midi_out_poll_wrapper &self, std::string_view name) { return self.impl.open_virtual_port(name); }) + .def("set_port_name", [](libremidi::midi_out_poll_wrapper &self, std::string_view name) { return self.impl.set_port_name(name); }) + .def("close_port", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.close_port(); }) + .def("is_port_open", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.is_port_open(); }) + .def("is_port_connected", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.is_port_connected(); }) + .def("absolute_timestamp", [](libremidi::midi_out_poll_wrapper &self) { return self.impl.current_time(); }) // clang-format off - .def("send_message", [](libremidi::midi_out_poll_wrapper &self, const libremidi::message& m) { return self.midi_out.send_message(m); }) - .def("send_message", [](libremidi::midi_out_poll_wrapper &self, const unsigned char* m, size_t size) { return self.midi_out.send_message(m, size); }) - .def("send_message", [](libremidi::midi_out_poll_wrapper &self, std::span m) { return self.midi_out.send_message(m); }) - .def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0) { return self.midi_out.send_message(b0); }) - .def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0, unsigned char b1) { return self.midi_out.send_message(b0, b1); }) - .def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0, unsigned char b1, unsigned char b2) { return self.midi_out.send_message(b0, b1, b2); }) - - .def("schedule_message", [](libremidi::midi_out_poll_wrapper &self, int64_t t, const unsigned char* m, size_t size) { return self.midi_out.schedule_message(t, m, size); }) - - .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, const libremidi::ump& m) { return self.midi_out.send_ump(m); }) - .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, const uint32_t* ump, size_t size) { return self.midi_out.send_ump(ump, size); }) - .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, std::span m) { return self.midi_out.send_ump(m); }) - .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0) { return self.midi_out.send_ump(u0); }) - .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0, uint32_t u1) { return self.midi_out.send_ump(u0, u1); }) - .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0, uint32_t u1, uint32_t u2) { return self.midi_out.send_ump(u0, u1, u2); }) - .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0, uint32_t u1, uint32_t u2, uint32_t u3) { return self.midi_out.send_ump(u0, u1, u2, u3); }) - - .def("schedule_message", [](libremidi::midi_out_poll_wrapper &self, int64_t t, const uint32_t* m, size_t size) { return self.midi_out.schedule_ump(t, m, size); }) + .def("send_message", [](libremidi::midi_out_poll_wrapper &self, const libremidi::message& m) { return self.impl.send_message(m); }) + .def("send_message", [](libremidi::midi_out_poll_wrapper &self, const unsigned char* m, size_t size) { return self.impl.send_message(m, size); }) + .def("send_message", [](libremidi::midi_out_poll_wrapper &self, std::span m) { return self.impl.send_message(m); }) + .def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0) { return self.impl.send_message(b0); }) + .def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0, unsigned char b1) { return self.impl.send_message(b0, b1); }) + .def("send_message", [](libremidi::midi_out_poll_wrapper &self, unsigned char b0, unsigned char b1, unsigned char b2) { return self.impl.send_message(b0, b1, b2); }) + + .def("schedule_message", [](libremidi::midi_out_poll_wrapper &self, int64_t t, const unsigned char* m, size_t size) { return self.impl.schedule_message(t, m, size); }) + + .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, const libremidi::ump& m) { return self.impl.send_ump(m); }) + .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, const uint32_t* ump, size_t size) { return self.impl.send_ump(ump, size); }) + .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, std::span m) { return self.impl.send_ump(m); }) + .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0) { return self.impl.send_ump(u0); }) + .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0, uint32_t u1) { return self.impl.send_ump(u0, u1); }) + .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0, uint32_t u1, uint32_t u2) { return self.impl.send_ump(u0, u1, u2); }) + .def("send_ump", [](libremidi::midi_out_poll_wrapper &self, uint32_t u0, uint32_t u1, uint32_t u2, uint32_t u3) { return self.impl.send_ump(u0, u1, u2, u3); }) + + .def("schedule_message", [](libremidi::midi_out_poll_wrapper &self, int64_t t, const uint32_t* m, size_t size) { return self.impl.schedule_ump(t, m, size); }) // clang-format on .def("poll", &libremidi::midi_out_poll_wrapper::poll); diff --git a/tests/python/list_apis.py b/tests/python/list_apis.py new file mode 100644 index 00000000..47ec4e6e --- /dev/null +++ b/tests/python/list_apis.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +import pylibremidi as lm +lm.available_apis() +lm.available_ump_apis() +lm.get_version() +lm.midi1_default_api() +lm.midi2_default_api()