diff --git a/cmake/ArchivePythonStdlib.py b/cmake/ArchivePythonStdlib.py index 37556bb989..0f6f18393c 100644 --- a/cmake/ArchivePythonStdlib.py +++ b/cmake/ArchivePythonStdlib.py @@ -1,4 +1,6 @@ import os +import fnmatch +import sys import stat import shutil import hashlib @@ -7,6 +9,11 @@ from argparse import ArgumentParser +def uniquify(seq): + seen = set() + return [x for x in seq if x and x not in seen and not seen.add(x)] + + def file_hash(file): h = hashlib.md5() @@ -16,12 +23,41 @@ def file_hash(file): return h.hexdigest() -def make_archive(file, directory): +def cleanup_files(directory, file_patterns = None, folder_patterns = None): + for dirname, folders, files in os.walk(directory): + if folder_patterns: + for foldername in folders: + for pattern in folder_patterns: + if fnmatch.fnmatch(foldername, pattern): + shutil.rmtree(os.path.join(dirname, foldername)) + break + + if file_patterns: + for filename in files: + for pattern in file_patterns: + if fnmatch.fnmatch(filename, pattern): + os.remove(os.path.join(dirname, filename)) + break + + +def append_files(*args): archived_files = [] - for dirname, _, files in os.walk(directory): - for filename in files: - path = os.path.join(dirname, filename) - archived_files.append((path, os.path.relpath(path, directory))) + + for directory_or_file in args: + if os.path.isdir(directory_or_file): + for dirname, _, files in os.walk(directory_or_file): + for filename in files: + path = os.path.join(dirname, filename) + archived_files.append((path, os.path.relpath(path, directory_or_file))) + else: + assert os.path.isabs(directory_or_file) + archived_files.append((directory_or_file, os.path.split(directory_or_file)[1])) + + return archived_files + + +def make_archive(file, directory_or_file, *args): + archived_files = append_files(directory_or_file, *args) with zipfile.ZipFile(file, "w") as zf: for path, archive_path in sorted(archived_files): @@ -42,17 +78,22 @@ def make_archive(file, directory): parser.add_argument("-M", "--version-major", type=int, help="Major version number (integer).") parser.add_argument("-m", "--version-minor", type=int, help="Minor version number (integer).") parser.add_argument("-x", "--exclude-patterns", type=str, default=None, help="Excluded patterns (semicolon separated list).") + parser.add_argument("-p", "--pip-packages", type=str, default=None, help="Pip packages to install (semicolon separated list).") + parser.add_argument("-z", "--zip-python-executable", action="store_true", help="Store the python executable in an archive.") args = parser.parse_args() version = f"{args.version_major}.{args.version_minor}" version_nodot = f"{args.version_major}{args.version_minor}" - final_location: Path = args.output_folder / "python" - site_packages = final_location / "site-packages" base_python: Path = args.base_folder / "lib" / f"python{version}" - final_archive = args.output_folder / f"python{version_nodot}.zip" - temp_archive = args.output_folder / f"temp{version_nodot}.zip" + final_location: Path = args.output_folder / "python" + site_packages: Path = final_location / "site-packages" + + final_archive: Path = args.output_folder / f"python{version_nodot}.zip" + temp_archive: Path = args.output_folder / f"temp{version_nodot}.zip" + final_python_archive: Path = args.output_folder / f"python{version_nodot}_executable.zip" + temp_python_archive: Path = args.output_folder / f"temp{version_nodot}_executable.zip" base_patterns = [ "*.pyc", @@ -65,33 +106,79 @@ def make_archive(file, directory): "Tk*", "_tk*", "_test*", + "_xxtestfuzz*", + "doctest*", + "idlelib", + "lib2to3", "libpython*", "pkgconfig", - "idlelib", + "pydoc*", "site-packages", "test", - "turtledemo", + "turtle*", "LICENSE.txt", ] if args.exclude_patterns: - custom_patterns = [x.strip() for x in args.exclude_patterns.split(";")] - base_patterns += custom_patterns + base_patterns += [x.strip() for x in args.exclude_patterns.split(";")] + + ignored_files = shutil.ignore_patterns(*uniquify(base_patterns)) - ignored_files = shutil.ignore_patterns(*base_patterns) + #============================================================================================== print("cleaning up...") if final_location.exists(): shutil.rmtree(final_location) + #============================================================================================== + print("copying library...") - shutil.copytree(base_python, final_location, ignore=ignored_files, dirs_exist_ok=True) + shutil.copytree( + base_python, + final_location, + ignore=ignored_files, + ignore_dangling_symlinks=True, + dirs_exist_ok=True) os.makedirs(site_packages, exist_ok=True) - print("making archive...") + #============================================================================================== + + if args.pip_packages: + print("installing packages...") + packages = uniquify([x.strip() for x in args.pip_packages.split(";")]) + os.system(f"{sys.executable} -m pip install --no-compile --target={site_packages} {' '.join(packages)}") + + print("cleaning up packages...") + cleanup_files(site_packages, ["*.pyi"], ["tests", "bin"]) + + #============================================================================================== + + print("making packages archive...") + #if sys.platform == "darwin": + # os.system(f"xattr -cr {final_location}") + if os.path.exists(final_archive): make_archive(temp_archive, final_location) if file_hash(temp_archive) != file_hash(final_archive): shutil.copy(temp_archive, final_archive) else: make_archive(final_archive, final_location) + + #============================================================================================== + + if args.zip_python_executable: + print("making python executable archive...") + python_executable = os.path.realpath(sys.executable) + print(python_executable) + + # TODO (darwin) + # 1. otool -L python_executable + # 2. if this contains a Python shared library in the Frameworks/Python.framework whatever, copy that as well + # 3. fix install_name_tool on python_executable to use the local + + if os.path.exists(final_python_archive): + make_archive(temp_python_archive, python_executable) + if file_hash(temp_python_archive) != file_hash(final_python_archive): + shutil.copy(temp_python_archive, final_python_archive) + else: + make_archive(final_python_archive, python_executable) diff --git a/demos/plugin/CMakeLists.txt b/demos/plugin/CMakeLists.txt index f4119a8454..bb64b84a76 100644 --- a/demos/plugin/CMakeLists.txt +++ b/demos/plugin/CMakeLists.txt @@ -41,8 +41,13 @@ add_subdirectory (${MODULES_PATH} ./modules) if (APPLE) set (Python_ROOT_DIR "/Library/Frameworks/Python.framework/Versions/Current") endif() -set (Python_USE_STATIC_LIBS TRUE) +set (Python_USE_STATIC_LIBS FALSE) find_package (Python REQUIRED Interpreter Development.Embed) +message("-- Popsicle -- Python_FOUND: ${Python_FOUND}") +message("-- Popsicle -- Python_VERSION: ${Python_VERSION}") +message("-- Popsicle -- Python_Development.Embed_FOUND: ${Python_Development.Embed_FOUND}") +message("-- Popsicle -- Python_INCLUDE_DIRS: ${Python_INCLUDE_DIRS}") +message("-- Popsicle -- Python_LIBRARIES: ${Python_LIBRARIES}") # Setup the juce app juce_add_plugin ("${PROJECT_NAME}" @@ -56,19 +61,20 @@ juce_add_plugin ("${PROJECT_NAME}" NEEDS_MIDI_INPUT OFF NEEDS_MIDI_OUTPUT OFF IS_MIDI_EFFECT OFF - APP_SANDBOX_ENABLED ON + #APP_SANDBOX_ENABLED ON COPY_PLUGIN_AFTER_BUILD ON) juce_generate_juce_header (${PROJECT_NAME}) # Add the binary target for the python standard library -set (ADDITIONAL_IGNORED_PYTHON_PATTERNS "lib2to3" "pydoc_data" "_xxtestfuzz*") +set (ADDITIONAL_IGNORED_PYTHON_PATTERNS "email" "curses" "dbm" "html" "http" "unittest" "tomllib" "wsgiref" "xmlrpc") +set (ADDITIONAL_PYTHON_PACKAGES "numpy") set (PYTHON_STANDARD_LIBRARY "${CMAKE_CURRENT_BINARY_DIR}/python${Python_VERSION_MAJOR}${Python_VERSION_MINOR}.zip") add_custom_target ( ${PROJECT_NAME}_stdlib ${Python_EXECUTABLE} ${ROOT_PATH}/cmake/ArchivePythonStdlib.py -b ${Python_ROOT_DIR} -o ${CMAKE_CURRENT_BINARY_DIR} -M ${Python_VERSION_MAJOR} -m ${Python_VERSION_MINOR} - -x "\"${ADDITIONAL_IGNORED_PYTHON_PATTERNS}\"" + -x "\"${ADDITIONAL_IGNORED_PYTHON_PATTERNS}\"" -p "\"${ADDITIONAL_PYTHON_PACKAGES}\"" BYPRODUCTS ${PYTHON_STANDARD_LIBRARY}) add_dependencies (${PROJECT_NAME} ${PROJECT_NAME}_stdlib) @@ -82,8 +88,7 @@ target_sources (${PROJECT_NAME} PRIVATE PopsiclePluginProcessor.cpp PopsiclePluginProcessor.h) -#set_target_properties (${PROJECT_NAME} PROPERTIES JUCE_TARGET_KIND_STRING "App") -set_target_properties (${PROJECT_NAME} PROPERTIES CXX_STANDARD 17) +set_target_properties (${PROJECT_NAME} PROPERTIES CXX_STANDARD 20) set_target_properties (${PROJECT_NAME} PROPERTIES CXX_VISIBILITY_PRESET "hidden") set_target_properties (${PROJECT_NAME} PROPERTIES VISIBILITY_INLINES_HIDDEN TRUE) set_target_properties (${PROJECT_NAME} PROPERTIES POSITION_INDEPENDENT_CODE TRUE) @@ -91,7 +96,7 @@ set_target_properties (${PROJECT_NAME} PROPERTIES POSITION_INDEPENDENT_CODE TRUE if (APPLE) #set_target_properties (${PROJECT_NAME} PROPERTIES OSX_ARCHITECTURES "arm64;x86_64") #set_target_properties (BinaryData PROPERTIES OSX_ARCHITECTURES "arm64;x86_64") - target_link_options (${PROJECT_NAME} PRIVATE "-Wl,-weak_reference_mismatches,weak") + target_link_options (${PROJECT_NAME} PRIVATE "-Wl,-weak_reference_mismatches,weak -Wl,-undefined,dynamic_lookup") #set (LTO_CONFIGURATION "juce::juce_recommended_lto_flags") set (LTO_CONFIGURATION "") else() @@ -111,9 +116,8 @@ elseif (UNIX) endif() target_compile_definitions (${PROJECT_NAME} PRIVATE - #JUCE_STANDALONE_APPLICATION=1 JUCE_DISPLAY_SPLASH_SCREEN=0 - JUCE_MODAL_LOOPS_PERMITTED=1 + JUCE_MODAL_LOOPS_PERMITTED=0 JUCE_CATCH_UNHANDLED_EXCEPTIONS=0 JUCE_LOG_ASSERTIONS=1 JUCE_ALLOW_STATIC_NULL_VARIABLES=0 @@ -123,6 +127,9 @@ target_compile_definitions (${PROJECT_NAME} PRIVATE JUCE_SILENCE_XCODE_15_LINKER_WARNING=1 PYBIND11_DETAILED_ERROR_MESSAGES=1) +target_include_directories(${PROJECT_NAME} PRIVATE + ${Python_INCLUDE_DIRS}) + target_link_libraries (${PROJECT_NAME} PRIVATE #juce::juce_analytics juce::juce_audio_basics @@ -144,8 +151,9 @@ target_link_libraries (${PROJECT_NAME} PRIVATE #juce::juce_video juce::juce_recommended_config_flags juce::juce_recommended_warning_flags - Python::Python + #Python::Python popsicle::juce_python popsicle::juce_python_recommended_warning_flags BinaryData + ${Python_LIBRARIES} ${LTO_CONFIGURATION}) diff --git a/demos/plugin/PopsiclePluginProcessor.cpp b/demos/plugin/PopsiclePluginProcessor.cpp index 18a09f5f04..2dde423c4c 100644 --- a/demos/plugin/PopsiclePluginProcessor.cpp +++ b/demos/plugin/PopsiclePluginProcessor.cpp @@ -3,6 +3,10 @@ // ================================================================================================= +JUCE_PYTHON_DEFINE_EMBEDDED_MODULES + +// ================================================================================================= + PYBIND11_EMBEDDED_MODULE(custom, m) { namespace py = pybind11; @@ -24,38 +28,19 @@ AudioPluginAudioProcessor::AudioPluginAudioProcessor() .withOutput ("Output", juce::AudioChannelSet::stereo(), true) #endif ) -/* , engine (popsicle::ScriptEngine::prepareScriptingHome ( - juce::JUCEApplication::getInstance()->getApplicationName(), - juce::File::getSpecialLocation (juce::File::tempDirectory), - [](const char* resourceName) -> juce::MemoryBlock - { - int dataSize = 0; - auto data = BinaryData::getNamedResource (resourceName, dataSize); - return { data, static_cast (dataSize) }; - }))*/ + , Thread ("Popsicle Python Thread") { - pybind11::dict locals; - //locals["custom"] = pybind11::module_::import ("custom"); - locals["juce"] = pybind11::module_::import (popsicle::PythonModuleName); - //locals["this"] = pybind11::cast (this); - - auto result = engine.runScript (R"( -# import sys - -# An example of scriptable self -print("Scripting JUCE!") - -#this.text = "Popsicle " + sys.version.split(" ")[0] -#this.setOpaque(True) -#this.setSize(600, 300) - )", locals); - - if (result.failed()) - std::cout << result.getErrorMessage(); + startThread(); } AudioPluginAudioProcessor::~AudioPluginAudioProcessor() { + signalThreadShouldExit(); + + audioReady.arrive_and_wait(); + backgroundReady.arrive_and_wait(); + + waitForThreadToExit(10000); } // ================================================================================================= @@ -126,34 +111,15 @@ void AudioPluginAudioProcessor::changeProgramName (int index, const juce::String // ================================================================================================= -void AudioPluginAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) -{ - // Use this method as the place to do any pre-playback - // initialisation that you need.. - juce::ignoreUnused (sampleRate, samplesPerBlock); -} - -void AudioPluginAudioProcessor::releaseResources() -{ - // When playback stops, you can use this as an opportunity to free up any - // spare memory, etc. -} - bool AudioPluginAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const { #if JucePlugin_IsMidiEffect juce::ignoreUnused (layouts); return true; #else - // This is the place where you check if the layout is supported. - // In this template code we only support mono or stereo. - // Some plugin hosts, such as certain GarageBand versions, will only - // load plugins that support stereo bus layouts. - if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() - && layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) + if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) return false; - // This checks if the input layout matches the output layout #if ! JucePlugin_IsSynth if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet()) return false; @@ -163,38 +129,6 @@ bool AudioPluginAudioProcessor::isBusesLayoutSupported (const BusesLayout& layou #endif } -void AudioPluginAudioProcessor::processBlock (juce::AudioBuffer& buffer, - juce::MidiBuffer& midiMessages) -{ - juce::ignoreUnused (midiMessages); - - juce::ScopedNoDenormals noDenormals; - auto totalNumInputChannels = getTotalNumInputChannels(); - auto totalNumOutputChannels = getTotalNumOutputChannels(); - - // In case we have more outputs than inputs, this code clears any output - // channels that didn't contain input data, (because these aren't - // guaranteed to be empty - they may contain garbage). - // This is here to avoid people getting screaming feedback - // when they first compile a plugin, but obviously you don't need to keep - // this code if your algorithm always overwrites all the output channels. - for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) - buffer.clear (i, 0, buffer.getNumSamples()); - - // This is the place where you'd normally do the guts of your plugin's - // audio processing... - // Make sure to reset the state if your inner loop is processing - // the samples and the outer loop is handling the channels. - // Alternatively, you can process the samples with the channels - // interleaved by keeping the same state. - for (int channel = 0; channel < totalNumInputChannels; ++channel) - { - auto* channelData = buffer.getWritePointer (channel); - juce::ignoreUnused (channelData); - // ..do something to the data... - } -} - // ================================================================================================= bool AudioPluginAudioProcessor::hasEditor() const @@ -226,6 +160,120 @@ void AudioPluginAudioProcessor::setStateInformation (const void* data, int sizeI // ================================================================================================= +void AudioPluginAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) +{ + juce::ignoreUnused (sampleRate); + + audioBuffer.setSize (2, samplesPerBlock); + backgroundBuffer.setSize (2, samplesPerBlock); +} + +void AudioPluginAudioProcessor::releaseResources() +{ +} + +void AudioPluginAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) +{ + juce::ignoreUnused (midiMessages); + + juce::ScopedNoDenormals noDenormals; + + auto totalNumInputChannels = getTotalNumInputChannels(); + auto totalNumOutputChannels = getTotalNumOutputChannels(); + + for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) + buffer.clear (i, 0, buffer.getNumSamples()); + + audioBuffer.copyFrom(0, 0, buffer, 0, 0, buffer.getNumSamples()); + audioBuffer.copyFrom(1, 0, buffer, 1, 0, buffer.getNumSamples()); + + if (isThreadRunning() && ! threadShouldExit()) + { + audioReady.arrive_and_wait(); + + std::swap(audioBuffer, backgroundBuffer); + + backgroundReady.arrive_and_wait(); + } + + buffer.clear(); +} + +// ================================================================================================= + +void AudioPluginAudioProcessor::run() +{ + auto tempPath = juce::File::getSpecialLocation (juce::File::tempDirectory); + tempPath.deleteRecursively(); + + auto engine = popsicle::ScriptEngine (popsicle::ScriptEngine::prepareScriptingHome ( + juce::JUCEApplication::getInstance()->getApplicationName(), + tempPath, + [](const char* resourceName) -> juce::MemoryBlock + { + int dataSize = 0; + auto data = BinaryData::getNamedResource (resourceName, dataSize); + return { data, static_cast (dataSize) }; + })); + + pybind11::dict locals; + locals["custom"] = pybind11::module_::import ("custom"); + locals["juce"] = pybind11::module_::import (popsicle::PythonModuleName); + //locals["this"] = pybind11::cast (this); + + pybind11::dict globals = pybind11::globals(); + +/* +import threading + +def example_thread(should_stop): + import sys, time + while not should_stop.is_set(): + sys.stdout.write("x") + sys.stdout.flush() + time.sleep(1) + +should_stop = threading.Event() +t = threading.Thread(target=example_thread, args=(should_stop,)) +t.daemon = True +t.start() +*/ + { + auto result = engine.runScript (R"( +import numpy as np + +def initialise(): + import sys + print("Running Popsicle as Python" + ".".join(sys.version.split(".")[:2])) + +def run(audioBuffer): + return audioBuffer + +def finalise(): + print("\nFinished Running Popsicle") + + )", locals, globals); + + if (result.failed()) + std::cout << result.getErrorMessage(); + } + + globals["initialise"](); + + while (! threadShouldExit()) + { + audioReady.arrive_and_wait(); + backgroundReady.arrive_and_wait(); + + auto maxValue = globals["run"](backgroundBuffer); + std::cout << pybind11::str(maxValue) << std::endl; + } + + globals["finalise"](); +} + +// ================================================================================================= + juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new AudioPluginAudioProcessor(); diff --git a/demos/plugin/PopsiclePluginProcessor.h b/demos/plugin/PopsiclePluginProcessor.h index 85c5b96721..76a7be8782 100644 --- a/demos/plugin/PopsiclePluginProcessor.h +++ b/demos/plugin/PopsiclePluginProcessor.h @@ -2,8 +2,11 @@ #include "JuceHeader.h" +#include +#include + //============================================================================== -class AudioPluginAudioProcessor : public juce::AudioProcessor +class AudioPluginAudioProcessor : public juce::AudioProcessor, public juce::Thread { public: //============================================================================== @@ -16,7 +19,7 @@ class AudioPluginAudioProcessor : public juce::AudioProcessor bool isBusesLayoutSupported (const BusesLayout& layouts) const override; - void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; + void processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiBuffer) override; using AudioProcessor::processBlock; //============================================================================== @@ -42,8 +45,14 @@ class AudioPluginAudioProcessor : public juce::AudioProcessor void getStateInformation (juce::MemoryBlock& destData) override; void setStateInformation (const void* data, int sizeInBytes) override; + //============================================================================== + void run() override; + private: - popsicle::ScriptEngine engine; + AudioBuffer audioBuffer; + AudioBuffer backgroundBuffer; + std::barrier<> audioReady{ 2 }; + std::barrier<> backgroundReady{ 2 }; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioPluginAudioProcessor) diff --git a/modules/juce_python/bindings/ScriptJuceBindings.cpp b/modules/juce_python/bindings/ScriptJuceBindings.cpp index 7e7aeef9bd..93a3a35750 100644 --- a/modules/juce_python/bindings/ScriptJuceBindings.cpp +++ b/modules/juce_python/bindings/ScriptJuceBindings.cpp @@ -16,8 +16,7 @@ * OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. */ -#include "../utilities/PyBind11Includes.h" - +#include "ScriptJuceBindings.h" #include "ScriptJuceCoreBindings.h" #if JUCE_MODULE_AVAILABLE_juce_events @@ -62,13 +61,10 @@ #endif // ================================================================================================= -#if JUCE_PYTHON_EMBEDDED_INTERPRETER -PYBIND11_EMBEDDED_MODULE (JUCE_PYTHON_MODULE_NAME, m) -#else -PYBIND11_MODULE (JUCE_PYTHON_MODULE_NAME, m) -#endif +namespace popsicle { +void registerMainModule(pybind11::module_& m) { -#if JUCE_MAC +#if JUCE_MAC && ! JUCE_MODULE_AVAILABLE_juce_audio_plugin_client juce::Process::setDockIconVisible (false); #endif @@ -115,3 +111,17 @@ PYBIND11_MODULE (JUCE_PYTHON_MODULE_NAME, m) popsicle::Bindings::registerJuceAudioUtilsBindings (m); #endif } +} // namespace popsicle + +// ================================================================================================= + +#if ! JUCE_MODULE_AVAILABLE_juce_audio_plugin_client +#if JUCE_PYTHON_EMBEDDED_INTERPRETER +PYBIND11_EMBEDDED_MODULE (JUCE_PYTHON_MODULE_NAME, m) +#else +PYBIND11_MODULE (JUCE_PYTHON_MODULE_NAME, m) +#endif +{ + popsicle::registerMainModule (m); +} +#endif diff --git a/modules/juce_python/bindings/ScriptJuceBindings.h b/modules/juce_python/bindings/ScriptJuceBindings.h new file mode 100644 index 0000000000..0092c9649c --- /dev/null +++ b/modules/juce_python/bindings/ScriptJuceBindings.h @@ -0,0 +1,23 @@ +/** + * juce_python - Python bindings for the JUCE framework. + * + * This file is part of the popsicle project. + * + * Copyright (c) 2024 - kunitoki + * + * popsicle is an open source library subject to commercial or open-source licensing. + * + * By using popsicle, you agree to the terms of the popsicle License Agreement, which can + * be found at https://raw.githubusercontent.com/kunitoki/popsicle/master/LICENSE + * + * Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). + * + * POPSICLE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED + * OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. + */ + +#include "../utilities/PyBind11Includes.h" + +namespace popsicle { +void registerMainModule(pybind11::module_& m); +} // namespace popsicle diff --git a/modules/juce_python/bindings/ScriptJuceGuiEntryPointsBindings.cpp b/modules/juce_python/bindings/ScriptJuceGuiEntryPointsBindings.cpp index de83e90b4b..298c4b4f5a 100644 --- a/modules/juce_python/bindings/ScriptJuceGuiEntryPointsBindings.cpp +++ b/modules/juce_python/bindings/ScriptJuceGuiEntryPointsBindings.cpp @@ -160,8 +160,6 @@ void registerJuceGuiEntryPointsBindings (py::module_& m) systemExit(); }, "applicationType"_a, "catchExceptionsAndContinue"_a = false); -#endif - // ================================================================================================= struct PyTestableApplication @@ -265,6 +263,8 @@ void registerJuceGuiEntryPointsBindings (py::module_& m) return std::addressof (self); }, py::return_value_policy::reference) ; + +#endif } } // namespace popsicle::Bindings diff --git a/modules/juce_python/juce_python.h b/modules/juce_python/juce_python.h index 238c59be5b..19221d56d7 100644 --- a/modules/juce_python/juce_python.h +++ b/modules/juce_python/juce_python.h @@ -82,12 +82,20 @@ #define JUCE_PYTHON_MODULE_NAME popsicle #endif +/** + * @brief Custom logs module name, it's possible to change but beware to update your `import` statements ! + */ +#ifndef JUCE_PYTHON_LOGS_MODULE_NAME + #define JUCE_PYTHON_LOGS_MODULE_NAME __popsicle__ +#endif + /** * @brief Custom python module name as string. */ namespace popsicle { static inline constexpr const char* const PythonModuleName = JUCE_PYTHON_STRINGIFY (JUCE_PYTHON_MODULE_NAME); +static inline constexpr const char* const PythonLogsModuleName = JUCE_PYTHON_STRINGIFY (JUCE_PYTHON_LOGS_MODULE_NAME); } // namespace popsicle @@ -99,6 +107,23 @@ static inline constexpr const char* const PythonModuleName = JUCE_PYTHON_STRINGI #error When building juce_python with JUCE_PYTHON_EMBEDDED_INTERPRETER=0 it is mandatory to also set JUCE_MODAL_LOOPS_PERMITTED=1 #endif +//============================================================================== +/** + * @brief Use `JUCE_PYTHON_DEFINE_EMBEDDED_MODULES` when defining a plugin. + * + * Because JUCE plugins are compiled with a shared static library, the embedded modules defined in the library are discarded by the linker + * when building the final plugin shared library. + * + * @see https://github.com/pybind/pybind11/discussions/4619 for moe information. + */ +#if JUCE_MODULE_AVAILABLE_juce_audio_plugin_client + #define JUCE_PYTHON_DEFINE_EMBEDDED_MODULES \ + PYBIND11_EMBEDDED_MODULE (JUCE_PYTHON_MODULE_NAME, m) { popsicle::registerMainModule (m); } \ + PYBIND11_EMBEDDED_MODULE (JUCE_PYTHON_LOGS_MODULE_NAME, m) { popsicle::registerLoggingModule (m); } +#else + #define JUCE_PYTHON_DEFINE_EMBEDDED_MODULES +#endif + //============================================================================== #include "scripting/ScriptException.h" @@ -109,4 +134,7 @@ static inline constexpr const char* const PythonModuleName = JUCE_PYTHON_STRINGI #include "utilities/CrashHandling.h" #include "utilities/PythonInterop.h" +#include "bindings/ScriptJuceBindings.h" #include "bindings/ScriptJuceCoreBindings.h" + +#include "modules/ScriptPopsicleLogsModule.h" diff --git a/modules/juce_python/juce_python_modules.cpp b/modules/juce_python/juce_python_modules.cpp index dbcf4f2cc2..7bfe1684bd 100644 --- a/modules/juce_python/juce_python_modules.cpp +++ b/modules/juce_python/juce_python_modules.cpp @@ -18,4 +18,4 @@ #include "juce_python.h" -#include "modules/ScriptPopsicleModule.cpp" +#include "modules/ScriptPopsicleLogsModule.cpp" diff --git a/modules/juce_python/modules/ScriptPopsicleModule.cpp b/modules/juce_python/modules/ScriptPopsicleLogsModule.cpp similarity index 89% rename from modules/juce_python/modules/ScriptPopsicleModule.cpp rename to modules/juce_python/modules/ScriptPopsicleLogsModule.cpp index f4264b5cc0..ac2d672ee5 100644 --- a/modules/juce_python/modules/ScriptPopsicleModule.cpp +++ b/modules/juce_python/modules/ScriptPopsicleLogsModule.cpp @@ -16,6 +16,8 @@ * OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. */ +#include "ScriptPopsicleLogsModule.h" + #if JUCE_PYTHON_EMBEDDED_INTERPRETER #define JUCE_PYTHON_INCLUDE_PYBIND11_STL @@ -24,7 +26,8 @@ #include #include -PYBIND11_EMBEDDED_MODULE(__popsicle__, m) +namespace popsicle { +void registerLoggingModule (pybind11::module_& m) { namespace py = pybind11; @@ -70,5 +73,13 @@ PYBIND11_EMBEDDED_MODULE(__popsicle__, m) sys.attr ("stderr") = popsicleSys.attr ("__saved_stderr__"); }); } +} // namespace popsicle + +#if ! JUCE_MODULE_AVAILABLE_juce_audio_plugin_client +PYBIND11_EMBEDDED_MODULE(JUCE_PYTHON_LOGS_MODULE_NAME, m) +{ + popsicle::registerLoggingModule (m); +} +#endif #endif diff --git a/modules/juce_python/modules/ScriptPopsicleLogsModule.h b/modules/juce_python/modules/ScriptPopsicleLogsModule.h new file mode 100644 index 0000000000..0cd94a7271 --- /dev/null +++ b/modules/juce_python/modules/ScriptPopsicleLogsModule.h @@ -0,0 +1,23 @@ +/** + * juce_python - Python bindings for the JUCE framework. + * + * This file is part of the popsicle project. + * + * Copyright (c) 2024 - kunitoki + * + * popsicle is an open source library subject to commercial or open-source licensing. + * + * By using popsicle, you agree to the terms of the popsicle License Agreement, which can + * be found at https://raw.githubusercontent.com/kunitoki/popsicle/master/LICENSE + * + * Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). + * + * POPSICLE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED + * OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. + */ + +#include "../utilities/PyBind11Includes.h" + +namespace popsicle { +void registerLoggingModule (pybind11::module_& m); +} // namespace popsicle diff --git a/modules/juce_python/scripting/ScriptUtilities.cpp b/modules/juce_python/scripting/ScriptUtilities.cpp index 7827b79042..78785bccd7 100644 --- a/modules/juce_python/scripting/ScriptUtilities.cpp +++ b/modules/juce_python/scripting/ScriptUtilities.cpp @@ -27,7 +27,7 @@ namespace py = pybind11; ScriptStreamRedirection::ScriptStreamRedirection() noexcept { #if JUCE_PYTHON_EMBEDDED_INTERPRETER - sys = py::module::import ("__popsicle__"); + sys = py::module_::import (PythonLogsModuleName); sys.attr ("__redirect__")(); #endif