Skip to content
Open
65 changes: 64 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
}
Expand All @@ -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,
}
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down
28 changes: 28 additions & 0 deletions conanfile_pip.py
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 10 additions & 2 deletions pyUvula/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
)
62 changes: 62 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"