Browse Source

python: very barebones SDL2 & GLFW applications.

Similarly to the windowless ones, one of them gets picked up to be
platform.Application.
pull/1/head
Vladimír Vondruš 7 years ago
parent
commit
33998df685
  1. 94
      modules/FindGLFW.cmake
  2. 173
      modules/FindSDL2.cmake
  3. 22
      src/python/magnum/platform/CMakeLists.txt
  4. 8
      src/python/magnum/platform/__init__.py
  5. 72
      src/python/magnum/platform/application.h
  6. 68
      src/python/magnum/platform/glfw.cpp
  7. 72
      src/python/magnum/platform/sdl2.cpp
  8. 2
      src/python/setup.py.cmake

94
modules/FindGLFW.cmake

@ -0,0 +1,94 @@
#.rst:
# Find GLFW
# ---------
#
# Finds the GLFW library using its cmake config if that exists, otherwise
# falls back to finding it manually. This module defines:
#
# GLFW_FOUND - True if GLFW library is found
# GLFW::GLFW - GLFW imported target
#
# Additionally, in case the config was not found, these variables are defined
# for internal usage:
#
# GLFW_LIBRARY - GLFW library
# GLFW_INCLUDE_DIR - Root include dir
#
#
# This file is part of Magnum.
#
# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019
# Vladimír Vondruš <mosra@centrum.cz>
# Copyright © 2016 Jonathan Hale <squareys@googlemail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
# GLFW installs cmake package config files to shared/ folder which handles
# dependencies in case GLFW is built statically. Try to find first, quietly, so
# it doesn't print loud messages when it's not found, since that's okay.
find_package(glfw3 CONFIG QUIET)
if(TARGET glfw)
if(NOT TARGET GLFW::GLFW)
# Aliases of (global) targets are only supported in CMake 3.11, so we
# work around it by this. This is easier than fetching all possible
# properties (which are impossible to track of) and then attempting to
# rebuild them into a new target.
add_library(GLFW::GLFW INTERFACE IMPORTED)
set_target_properties(GLFW::GLFW PROPERTIES INTERFACE_LINK_LIBRARIES glfw)
endif()
# Just to make FPHSA print some meaningful location, nothing else
get_target_property(_GLFW_INTERFACE_INCLUDE_DIRECTORIES glfw INTERFACE_INCLUDE_DIRECTORIES)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args("GLFW" DEFAULT_MSG
_GLFW_INTERFACE_INCLUDE_DIRECTORIES)
return()
endif()
# In case no config file was found, try manually finding the library.
find_library(GLFW_LIBRARY NAMES glfw glfw3)
# Include dir
find_path(GLFW_INCLUDE_DIR
NAMES GLFW/glfw3.h)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args("GLFW" DEFAULT_MSG
GLFW_LIBRARY
GLFW_INCLUDE_DIR)
if(NOT TARGET GLFW::GLFW)
add_library(GLFW::GLFW UNKNOWN IMPORTED)
# Work around BUGGY framework support on macOS
# https://cmake.org/Bug/view.php?id=14105
if(CORRADE_TARGET_APPLE AND ${GLFW_LIBRARY} MATCHES "\\.framework$")
set_property(TARGET GLFW::GLFW PROPERTY IMPORTED_LOCATION ${GLFW_LIBRARY}/GLFW)
else()
set_property(TARGET GLFW::GLFW PROPERTY IMPORTED_LOCATION ${GLFW_LIBRARY})
endif()
set_property(TARGET GLFW::GLFW PROPERTY
INTERFACE_INCLUDE_DIRECTORIES ${GLFW_INCLUDE_DIR})
endif()
mark_as_advanced(GLFW_LIBRARY GLFW_INCLUDE_DIR)

173
modules/FindSDL2.cmake

@ -0,0 +1,173 @@
#.rst:
# Find SDL2
# ---------
#
# Finds the SDL2 library. This module defines:
#
# SDL2_FOUND - True if SDL2 library is found
# SDL2::SDL2 - SDL2 imported target
#
# Additionally these variables are defined for internal usage:
#
# SDL2_LIBRARY_DEBUG - SDL2 debug library, if found
# SDL2_LIBRARY_RELEASE - SDL2 release library, if found
# SDL2_INCLUDE_DIR - Root include dir
#
#
# This file is part of Magnum.
#
# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019
# Vladimír Vondruš <mosra@centrum.cz>
# Copyright © 2018 Jonathan Hale <squareys@googlemail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
# In Emscripten SDL is linked automatically, thus no need to find the library.
# Also the includes are in SDL subdirectory, not SDL2.
if(CORRADE_TARGET_EMSCRIPTEN)
set(_SDL2_PATH_SUFFIXES SDL)
else()
set(_SDL2_PATH_SUFFIXES SDL2)
if(WIN32)
# Precompiled libraries for MSVC are in x86/x64 subdirectories
if(MSVC)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(_SDL2_LIBRARY_PATH_SUFFIX lib/x64)
elseif(CMAKE_SIZEOF_VOID_P EQUAL 4)
set(_SDL2_LIBRARY_PATH_SUFFIX lib/x86)
endif()
# Both includes and libraries for MinGW are in some directory deep
# inside. There's also a CMake config file but it has HARDCODED path
# to /opt/local/i686-w64-mingw32, which doesn't make ANY SENSE,
# especially on Windows.
elseif(MINGW)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(_SDL2_LIBRARY_PATH_SUFFIX x86_64-w64-mingw32/lib)
list(APPEND _SDL2_PATH_SUFFIXES x86_64-w64-mingw32/include/SDL2)
elseif(CMAKE_SIZEOF_VOID_P EQUAL 4)
set(_SDL2_LIBRARY_PATH_SUFFIX i686-w64-mingw32/lib)
list(APPEND _SDL2_PATH_SUFFIXES i686-w64-mingw32/include/SDL2)
endif()
endif()
endif()
find_library(SDL2_LIBRARY_RELEASE
# Compiling SDL2 from scratch on macOS creates dead libSDL2.so symlink
# which CMake somehow prefers before the SDL2-2.0.dylib file. Making
# the dylib first so it is preferred. Not sure how this maps to debug
# config though :/
NAMES SDL2-2.0 SDL2
PATH_SUFFIXES ${_SDL2_LIBRARY_PATH_SUFFIX})
find_library(SDL2_LIBRARY_DEBUG
NAMES SDL2d
PATH_SUFFIXES ${_SDL2_LIBRARY_PATH_SUFFIX})
# FPHSA needs one of the _DEBUG/_RELEASE variables to check that the
# library was found -- using SDL_LIBRARY, which will get populated by
# select_library_configurations() below.
set(SDL2_LIBRARY_NEEDED SDL2_LIBRARY)
endif()
include(SelectLibraryConfigurations)
select_library_configurations(SDL2)
# Include dir
find_path(SDL2_INCLUDE_DIR
# We must search file which is present only in SDL2 and not in SDL1.
# Apparently when both SDL.h and SDL_scancode.h are specified, CMake is
# happy enough that it found SDL.h and doesn't bother about the other.
#
# On macOS, where the includes are not in SDL2/SDL.h form (which would
# solve this issue), but rather SDL2.framework/Headers/SDL.h, CMake might
# find SDL.framework/Headers/SDL.h if SDL1 is installed, which is wrong.
NAMES SDL_scancode.h
PATH_SUFFIXES ${_SDL2_PATH_SUFFIXES})
# iOS dependencies
if(CORRADE_TARGET_IOS)
set(_SDL2_FRAMEWORKS
AudioToolbox
AVFoundation
CoreGraphics
CoreMotion
Foundation
GameController
QuartzCore
UIKit)
set(_SDL2_FRAMEWORK_LIBRARIES )
foreach(framework ${_SDL2_FRAMEWORKS})
find_library(_SDL2_${framework}_LIBRARY ${framework})
mark_as_advanced(_SDL2_${framework}_LIBRARY)
list(APPEND _SDL2_FRAMEWORK_LIBRARIES ${_SDL2_${framework}_LIBRARY})
list(APPEND _SDL2_FRAMEWORK_LIBRARY_NAMES _SDL2_${framework}_LIBRARY)
endforeach()
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args("SDL2" DEFAULT_MSG
${SDL2_LIBRARY_NEEDED}
${_SDL2_FRAMEWORK_LIBRARY_NAMES}
SDL2_INCLUDE_DIR)
if(NOT TARGET SDL2::SDL2)
if(SDL2_LIBRARY_NEEDED)
add_library(SDL2::SDL2 UNKNOWN IMPORTED)
# Work around BUGGY framework support on macOS
# https://cmake.org/Bug/view.php?id=14105
if(CORRADE_TARGET_APPLE AND SDL2_LIBRARY_RELEASE MATCHES "\\.framework$")
set_property(TARGET SDL2::SDL2 APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_property(TARGET SDL2::SDL2 PROPERTY IMPORTED_LOCATION_RELEASE ${SDL2_LIBRARY_RELEASE}/SDL2)
else()
if(SDL2_LIBRARY_RELEASE)
set_property(TARGET SDL2::SDL2 APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_property(TARGET SDL2::SDL2 PROPERTY IMPORTED_LOCATION_RELEASE ${SDL2_LIBRARY_RELEASE})
endif()
if(SDL2_LIBRARY_DEBUG)
set_property(TARGET SDL2::SDL2 APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_property(TARGET SDL2::SDL2 PROPERTY IMPORTED_LOCATION_DEBUG ${SDL2_LIBRARY_DEBUG})
endif()
endif()
# Link additional `dl` and `pthread` libraries required by a static
# build of SDL on Unixy platforms (except Apple, where it is most
# probably some frameworks instead)
if(CORRADE_TARGET_UNIX AND NOT CORRADE_TARGET_APPLE AND SDL2_LIBRARY MATCHES "${CMAKE_STATIC_LIBRARY_SUFFIX}$")
find_package(Threads)
set_property(TARGET SDL2::SDL2 APPEND PROPERTY
INTERFACE_LINK_LIBRARIES ${CMAKE_THREAD_LIBS_INIT} ${CMAKE_DL_LIBS})
endif()
# Link frameworks on iOS
if(CORRADE_TARGET_IOS)
set_property(TARGET SDL2::SDL2 APPEND PROPERTY
INTERFACE_LINK_LIBRARIES ${_SDL2_FRAMEWORK_LIBRARIES})
endif()
else()
add_library(SDL2::SDL2 INTERFACE IMPORTED)
endif()
set_property(TARGET SDL2::SDL2 PROPERTY
INTERFACE_INCLUDE_DIRECTORIES ${SDL2_INCLUDE_DIR})
endif()
mark_as_advanced(SDL2_INCLUDE_DIR)

22
src/python/magnum/platform/CMakeLists.txt

@ -25,9 +25,31 @@
# *Not* REQUIRED
find_package(Magnum COMPONENTS
GlfwApplication
Sdl2Application
WindowlessEglApplication
WindowlessGlxApplication)
if(Magnum_GlfwApplication_FOUND)
pybind11_add_module(magnum_platform_glfw glfw.cpp)
target_link_libraries(magnum_platform_glfw PRIVATE Magnum::GlfwApplication)
target_include_directories(magnum_platform_glfw PRIVATE ${CMAKE_SOURCE_DIR}/src/python)
set_target_properties(magnum_platform_glfw PROPERTIES
FOLDER "python/platform"
OUTPUT_NAME "glfw"
LIBRARY_OUTPUT_DIRECTORY ${output_dir}/magnum/platform)
endif()
if(Magnum_Sdl2Application_FOUND)
pybind11_add_module(magnum_platform_sdl2 sdl2.cpp)
target_link_libraries(magnum_platform_sdl2 PRIVATE Magnum::Sdl2Application)
target_include_directories(magnum_platform_sdl2 PRIVATE ${CMAKE_SOURCE_DIR}/src/python)
set_target_properties(magnum_platform_sdl2 PROPERTIES
FOLDER "python/platform"
OUTPUT_NAME "sdl2"
LIBRARY_OUTPUT_DIRECTORY ${output_dir}/magnum/platform)
endif()
if(Magnum_WindowlessEglApplication_FOUND)
pybind11_add_module(magnum_platform_egl egl.cpp)
target_link_libraries(magnum_platform_egl PRIVATE Magnum::WindowlessEglApplication)

8
src/python/magnum/platform/__init__.py

@ -32,3 +32,11 @@ except ImportError: # pragma: no cover
from .egl import WindowlessApplication
except ImportError:
pass
try:
from .sdl2 import Application
except ImportError: # pragma: no cover
try:
from .glfw import Application
except ImportError:
pass

72
src/python/magnum/platform/application.h

@ -0,0 +1,72 @@
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include <pybind11/pybind11.h>
#include "magnum/bootstrap.h"
namespace magnum { namespace platform {
template<class T, class Trampoline> void application(py::class_<T, Trampoline>& c) {
py::class_<typename T::Configuration> configuration{c, "Configuration", "Configuration"};
configuration
.def(py::init())
.def_property("title", &T::Configuration::title,
[](typename T::Configuration& self, const std::string& title) {
self.setTitle(title);
}, "Window title")
.def_property("size", &T::Configuration::size,
[](typename T::Configuration& self, const Vector2i& size) {
self.setSize(size);
}, "Window size");
/** @todo others */
py::class_<typename T::GLConfiguration> glConfiguration{c, "GLConfiguration", "OpenGL context configuration"};
glConfiguration
.def(py::init());
/** @todo others */
c
/* Constructor */
.def(py::init<const typename T::Configuration&, const typename T::GLConfiguration&>(), py::arg("configuration") = typename T::Configuration{}, py::arg("gl_configuration") = typename T::GLConfiguration{},
"Constructor")
/** @todo the nocreate ones */
/* Basic things */
.def("exec", &T::exec, "Execute application main loop")
.def("exit", &T::exit, "Exit application main loop")
/* Screen handling */
.def("swap_buffers", &T::swapBuffers, "Swap buffers")
/** @todo setMinimalLoopPeriod, needs a getter */
.def("redraw", &T::redraw, "Redraw immediately")
/* Event handlers */
.def("draw_event", &T::drawEvent, "Draw event")
/** @todo more */
;
}
}}

68
src/python/magnum/platform/glfw.cpp

@ -0,0 +1,68 @@
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include <pybind11/pybind11.h>
#include <Magnum/Platform/GlfwApplication.h>
#include "magnum/bootstrap.h"
#include "magnum/platform/application.h"
namespace magnum { namespace platform { namespace {
int argc = 0;
void glfw(py::module& m) {
struct PublicizedApplication: Platform::Application {
explicit PublicizedApplication(const Configuration& configuration, const GLConfiguration& glConfiguration): Platform::Application{Arguments{argc, nullptr}, configuration, glConfiguration} {}
void drawEvent() override = 0;
};
struct PyApplication: PublicizedApplication {
using PublicizedApplication::PublicizedApplication;
void drawEvent() override {
PYBIND11_OVERLOAD_PURE_NAME(
void,
PublicizedApplication,
"draw_event",
drawEvent
);
}
};
py::class_<PublicizedApplication, PyApplication> glfwApplication{m, "Application", "GLFW application"};
/** @todo def_property_writeonly for swap_interval */
application(glfwApplication);
}
}}}
PYBIND11_MODULE(glfw, m) {
m.doc() = "GLFW-based platform integration";
magnum::platform::glfw(m);
}

72
src/python/magnum/platform/sdl2.cpp

@ -0,0 +1,72 @@
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include <pybind11/pybind11.h>
#include <Magnum/Platform/Sdl2Application.h>
#include "magnum/bootstrap.h"
#include "magnum/platform/application.h"
namespace magnum { namespace platform { namespace {
int argc = 0;
void sdl2(py::module& m) {
struct PublicizedApplication: Platform::Application {
explicit PublicizedApplication(const Configuration& configuration, const GLConfiguration& glConfiguration): Platform::Application{Arguments{argc, nullptr}, configuration, glConfiguration} {}
void drawEvent() override = 0;
};
struct PyApplication: PublicizedApplication {
using PublicizedApplication::PublicizedApplication;
void drawEvent() override {
PYBIND11_OVERLOAD_PURE_NAME(
void,
PublicizedApplication,
"draw_event",
drawEvent
);
}
};
py::class_<PublicizedApplication, PyApplication> sdl2application{m, "Application", "SDL2 application"};
sdl2application
.def_property("swap_interval", &PyApplication::swapInterval,
[](PublicizedApplication& self, Int interval) {
self.setSwapInterval(interval);
}, "Swap interval");
application(sdl2application);
}
}}}
PYBIND11_MODULE(sdl2, m) {
m.doc() = "SDL2-based platform integration";
magnum::platform::sdl2(m);
}

2
src/python/setup.py.cmake

@ -37,6 +37,8 @@ extension_paths = {
'magnum.shaders': '$<$<TARGET_EXISTS:magnum_shaders>:$<TARGET_FILE:magnum_shaders>>',
'magnum.platform.egl': '$<$<TARGET_EXISTS:magnum_platform_egl>:$<TARGET_FILE:magnum_platform_egl>>',
'magnum.platform.glx': '$<$<TARGET_EXISTS:magnum_platform_glx>:$<TARGET_FILE:magnum_platform_glx>>',
'magnum.platform.glfw': '$<$<TARGET_EXISTS:magnum_platform_glfw>:$<TARGET_FILE:magnum_platform_glfw>>',
'magnum.platform.sdl2': '$<$<TARGET_EXISTS:magnum_platform_sdl2>:$<TARGET_FILE:magnum_platform_sdl2>>',
}
class TheExtensionIsAlreadyBuiltWhyThisHasToBeSoDamnComplicated(build_ext):

Loading…
Cancel
Save