diff --git a/CMakeLists.txt b/CMakeLists.txt index c24ee28..f11afe0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,70 @@ cmake_minimum_required(VERSION 3.23) + +# --- Bootstrap Conan dependencies when building as a pip package --- +# When invoked via `pip install` (scikit-build-core sets UVULA_PIP_BUILD=ON), +# auto-run `conan install` to fetch the C++ dependencies and wire up the +# toolchain before project() is called. +option(UVULA_PIP_BUILD "Auto-run 'conan install' to fetch deps for a pip build" OFF) +if (UVULA_PIP_BUILD AND NOT CMAKE_TOOLCHAIN_FILE AND NOT _UVULA_CONAN_BOOTSTRAPPED) + find_program(_UVULA_CONAN_EXE conan REQUIRED) + set(_UVULA_CONAN_OUTPUT "${CMAKE_BINARY_DIR}/conan_deps") + execute_process( + COMMAND "${_UVULA_CONAN_EXE}" profile detect --exist-ok + RESULT_VARIABLE _UVULA_CONAN_PROFILE_RC + ) + message(STATUS "UVULA_PIP_BUILD: running 'conan install' into ${_UVULA_CONAN_OUTPUT}") + execute_process( + COMMAND "${_UVULA_CONAN_EXE}" install "${CMAKE_CURRENT_LIST_DIR}/conanfile_pip.py" + --build=missing + --output-folder=${_UVULA_CONAN_OUTPUT} + -s build_type=Release + -s compiler.cppstd=20 + -c tools.cmake.cmaketoolchain:generator=Ninja + RESULT_VARIABLE _UVULA_CONAN_RC + ) + if (NOT _UVULA_CONAN_RC EQUAL 0) + message(FATAL_ERROR "conan install failed (exit ${_UVULA_CONAN_RC}). " + "Ensure Conan 2.x is installed and any private remotes " + "needed for Ultimaker packages are configured.") + endif() + file(GLOB_RECURSE _UVULA_TOOLCHAIN_FILES "${_UVULA_CONAN_OUTPUT}/*conan_toolchain.cmake") + if (NOT _UVULA_TOOLCHAIN_FILES) + message(FATAL_ERROR "conan install succeeded but no conan_toolchain.cmake was generated under ${_UVULA_CONAN_OUTPUT}") + endif() + list(GET _UVULA_TOOLCHAIN_FILES 0 _UVULA_TOOLCHAIN) + set(CMAKE_TOOLCHAIN_FILE "${_UVULA_TOOLCHAIN}" CACHE FILEPATH "Conan-generated toolchain" FORCE) + set(_UVULA_CONAN_BOOTSTRAPPED TRUE CACHE INTERNAL "Conan deps already fetched") + message(STATUS "UVULA_PIP_BUILD: using toolchain ${CMAKE_TOOLCHAIN_FILE}") +endif() + project(uvula) -find_package(standardprojectsettings REQUIRED) +if (UVULA_PIP_BUILD) + # The C++ sources use std::span and other C++20 features. The default + # conan profile typically selects gnu17, so force C++20 explicitly here. + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_CXX_EXTENSIONS OFF) + # libuvula is STATIC but gets linked into the pyUvula shared module. + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + + # Inline minimal replacements for the Ultimaker-internal + # `standardprojectsettings` helpers so a pip install doesn't need the + # private conan remote. Sanitizers and extensive warnings are no-ops here + # since a redistributable wheel shouldn't ship with them enabled anyway. + find_package(Threads) + function(use_threads target) + if (TARGET Threads::Threads) + target_link_libraries(${target} PUBLIC Threads::Threads) + endif() + endfunction() + function(enable_sanitizers target) + endfunction() + function(set_project_warnings target) + endfunction() +else() + find_package(standardprojectsettings REQUIRED) +endif() find_package(spdlog REQUIRED) find_package(range-v3 REQUIRED) find_package(clipper REQUIRED) diff --git a/README.md b/README.md index 7976365..8dedfb4 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,16 @@ cmake --build --preset conan-release The python bindings are built by default, but can be ignored by adding `-o with_python_bindings=False` when doing the setup with `conan`. +### Installing via pip + +The Python binding can also be installed directly from source with pip (no prior Conan setup required — the build backend installs Conan transparently and fetches the C++ dependencies from conancenter): + +```bash +pip install git+https://github.com/Ultimaker/libUvula.git +``` + +A C++20 compiler and CMake ≥ 3.23 must be available on the machine running the install. + Once built, just make sure you have the library in the path and call the `unwrap` function: ```python diff --git a/conanfile.py b/conanfile.py index 5254a39..137bb91 100644 --- a/conanfile.py +++ b/conanfile.py @@ -30,6 +30,7 @@ class UvulaConan(ConanFile): "fPIC": [True, False], "enable_extensive_warnings": [True, False], "with_python_bindings": [True, False], + "with_system_python": [True, False], "with_js_bindings": [True, False], "with_cli": [True, False], } @@ -38,6 +39,7 @@ class UvulaConan(ConanFile): "fPIC": True, "enable_extensive_warnings": False, "with_python_bindings": True, + "with_system_python": False, "with_js_bindings": False, "with_cli": False, } @@ -79,7 +81,7 @@ def config_options(self): self.options.with_js_bindings = True def configure(self): - if self.options.get_safe("with_python_bindings", False): + if self.options.get_safe("with_python_bindings", False) and not self.options.get_safe("with_system_python", False): self.options["cpython"].shared = True def layout(self): @@ -103,7 +105,7 @@ def requirements(self): self.requires("spdlog/1.15.1") self.requires("range-v3/0.12.0") self.requires("clipper/6.4.2@ultimaker/stable") - if self.options.get_safe("with_python_bindings", False): + if self.options.get_safe("with_python_bindings", False) and not self.options.get_safe("with_system_python", False): self.requires("cpython/3.12.2") self.requires("pybind11/2.11.1") if self.options.get_safe("with_cli", False): diff --git a/conanfile_pip.py b/conanfile_pip.py new file mode 100644 index 0000000..d398469 --- /dev/null +++ b/conanfile_pip.py @@ -0,0 +1,28 @@ +# Minimal Conan recipe used exclusively by the `pip install` path (see +# pyproject.toml / CMakeLists.txt UVULA_PIP_BUILD bootstrap). It mirrors the +# subset of `conanfile.py` needed to build the Python binding, but without the +# Emscripten-only `npmpackage` python_requires — which would otherwise force +# every conan operation to resolve an Ultimaker-internal package even when +# building the Python wheel. + +from conan import ConanFile +from conan.tools.cmake import cmake_layout + + +class UvulaPipBuildConan(ConanFile): + name = "uvula-pip-build" + settings = "os", "arch", "compiler", "build_type" + generators = "CMakeDeps", "CMakeToolchain" + + def requirements(self): + self.requires("spdlog/1.15.1") + self.requires("range-v3/0.12.0") + # The main conanfile pins Ultimaker's `clipper/6.4.2@ultimaker/stable` + # fork, which is hosted on a private remote. Fall back to the upstream + # `clipper/6.4.2` on conancenter so `pip install` works without that + # remote. If the build breaks on fork-specific API, configure the + # Ultimaker remote and pin the @ultimaker/stable revision here instead. + self.requires("clipper/6.4.2") + + def layout(self): + cmake_layout(self) diff --git a/pyUvula/CMakeLists.txt b/pyUvula/CMakeLists.txt index e9bb37f..b085c74 100644 --- a/pyUvula/CMakeLists.txt +++ b/pyUvula/CMakeLists.txt @@ -1,5 +1,5 @@ set(ENV{LD_LIBRARY_PATH} "${CMAKE_LIBRARY_PATH}:${LD_LIBRARY_PATH}") # Needed to ensure that CMake finds the Conan CPython library -find_package(Python COMPONENTS Interpreter Development) +find_package(Python3 COMPONENTS Interpreter Development.Module) find_package(pybind11 REQUIRED) pybind11_add_module(pyUvula pyUvula.cpp) @@ -9,6 +9,14 @@ if (NOT MSVC) endif() target_link_libraries(pyUvula PUBLIC libuvula ${NEEDED_DEPS}) target_compile_definitions(pyUvula PRIVATE PYUVULA_VERSION="${PYUVULA_VERSION}") -if (NOT MSVC AND NOT ${CMAKE_BUILD_TYPE} MATCHES Debug|RelWithDebInfo) +if (NOT MSVC AND NOT "${CMAKE_BUILD_TYPE}" MATCHES "Debug|RelWithDebInfo") pybind11_strip(pyUvula) endif () + +# Install rule for pip / scikit-build-core wheel packaging. +# Dropping the module at the wheel root makes `import pyUvula` work. +install(TARGETS pyUvula + LIBRARY DESTINATION . + RUNTIME DESTINATION . + COMPONENT pyUvula +) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..65e9dd9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,62 @@ +[build-system] +requires = [ + "scikit-build-core>=0.10", + "pybind11>=2.11,<3.0", + "conan>=2.7", +] +build-backend = "scikit_build_core.build" + +[project] +name = "pyUvula" +version = "1.0.1" +description = "UV-unwrapping library: normal-based face segmentation with xatlas chart packing." +readme = "README.md" +license = { file = "LICENSE" } +authors = [{ name = "UltiMaker" }] +requires-python = ">=3.9" +classifiers = [ + "Development Status :: 4 - Beta", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Programming Language :: C++", + "Programming Language :: Python :: 3", + "Topic :: Multimedia :: Graphics :: 3D Modeling", +] + +[project.urls] +Homepage = "https://github.com/Ultimaker/libUvula" +Source = "https://github.com/Ultimaker/libUvula" + +[tool.scikit-build] +minimum-version = "build-system.requires" +cmake.version = ">=3.23" +cmake.build-type = "Release" +cmake.args = ["-GNinja"] +build.targets = ["pyUvula"] +sdist.include = [ + "CMakeLists.txt", + "conanfile.py", + "conanfile_pip.py", + "conandata.yml", + "include/**", + "src/**", + "pyUvula/**", + "LICENSE", + "README.md", +] +sdist.exclude = [ + "UvulaJS", + "cli", + "demo.png", + ".github", +] + +[tool.scikit-build.cmake.define] +UVULA_PIP_BUILD = "ON" +UVULA_VERSION = "1.0.1" +PYUVULA_VERSION = "1.0.1" +WITH_PYTHON_BINDINGS = "ON" +WITH_JS_BINDINGS = "OFF" +WITH_CLI = "OFF" +EXTENSIVE_WARNINGS = "OFF"