-
Notifications
You must be signed in to change notification settings - Fork 68
Description
I'm transitioning from an old workflow based on CMakeLists.txt and setup.py to a more modern, cross-platform setup using scikit-build, pyproject.toml, and uv. Here's what
I’ve put together so far:
CMakeLists.txt:
cmake_minimum_required(VERSION 3.18)
# Set the project name first
project(hello_pybind11 LANGUAGES CXX)
# Enable faster builds with optimizations
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Use Release builds by default for better performance
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()
# Enable faster incremental builds
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Use ccache if available for faster rebuilds
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
message(STATUS "Using ccache: ${CCACHE_PROGRAM}")
endif()
# Find pybind11 - use quiet mode to reduce output
find_package(pybind11 REQUIRED QUIET)
# Add the module
pybind11_add_module(hello_pybind11 main.cpp)
# Set version info properly
if(NOT DEFINED EXAMPLE_VERSION_INFO OR EXAMPLE_VERSION_INFO STREQUAL "")
set(EXAMPLE_VERSION_INFO "0.1.0")
endif()
target_compile_definitions(hello_pybind11 PRIVATE VERSION_INFO=${EXAMPLE_VERSION_INFO})
# Cross-platform parallel compilation and optimizations
if(MSVC)
# Windows: Enable parallel compilation and faster linking
target_compile_options(hello_pybind11 PRIVATE
/MP # Multi-processor compilation
/bigobj # Large object files
)
# Use faster linker if available
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.15")
set_target_properties(hello_pybind11 PROPERTIES
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL"
)
endif()
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
# Linux/macOS: Enable optimizations
target_compile_options(hello_pybind11 PRIVATE
-Wall -Wextra
$<$<CONFIG:Debug>:-g -O0>
$<$<CONFIG:Release>:-O3 -DNDEBUG>
)
# Use faster linker if available
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
# Try to use lld if available
find_program(LLD_PROGRAM lld)
if(LLD_PROGRAM)
target_link_options(hello_pybind11 PRIVATE -fuse-ld=lld)
endif()
endif()
endif()
# Platform-specific settings
if(WIN32)
target_compile_definitions(hello_pybind11 PRIVATE _WIN32_WINNT=0x0601)
elseif(APPLE)
set_target_properties(hello_pybind11 PROPERTIES
MACOSX_RPATH ON
INSTALL_RPATH_USE_LINK_PATH ON
)
elseif(UNIX)
set_target_properties(hello_pybind11 PROPERTIES
INSTALL_RPATH_USE_LINK_PATH ON
)
endif()
# Enable detailed error messages only in debug mode
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(hello_pybind11 PRIVATE PYBIND11_DETAILED_ERROR_MESSAGES)
endif()
# Install target
install(TARGETS hello_pybind11 DESTINATION .)
Justfile:
default:
@just --list
setup:
@echo "🚀 Setting up development environment..."
uv venv
uv pip install build scikit-build-core pybind11
@echo "✅ Initial development install..."
uv pip install -e . --config-settings=build-dir=build
@echo "🎉 Setup complete! Use 'just dev' for fast iterations."
pre-dev:
@echo "⚡ Reconfiguring CMake with venv settings..."
@uv run cmake -S . -B build \
-Dpybind11_DIR=`uv run python -c "import pybind11; print(pybind11.get_cmake_dir())"` \
-DPython_EXECUTABLE=`uv run python -c "import sys; print(sys.executable)"` \
-DCMAKE_BUILD_TYPE=Release \
dev:
@echo "⚡ Building incrementally..."
@uv run cmake --build build --config Release -j 4
@echo "📦 Copying built module to site-packages..."
@uv run python -c "import shutil, os, site; shutil.copy2(os.path.join('.venv', 'Release', 'hello_pybind11.cp311-win_amd64.pyd'), site.getsitepackages()[0])"
check:
@echo "⚡ Quick import test..."
@uv run python -c "import hello_pybind11; print(hello_pybind11.hello_world()); print(f'5 + 3 = {hello_pybind11.add(5, 3)}')"
test file="test.py":
@echo "🧪 Running {{file}}..."
@uv run python {{file}}
rebuild:
@echo "🔄 Full rebuild..."
@python -c "import shutil, os; [shutil.rmtree(p, ignore_errors=True) for p in ['build'] if os.path.exists(p)]"
@python -c "import glob, os; [os.remove(f) for f in glob.glob('*.pyd') + glob.glob('*.so')]"
@python -c "import shutil, glob; [shutil.rmtree(p, ignore_errors=True) for p in glob.glob('*.egg-info')]"
uv pip install -e . --config-settings=build-dir=build --force-reinstall --no-deps
clean:
@echo "🧹 Cleaning..."
@python -c "import shutil, os; [shutil.rmtree(p, ignore_errors=True) for p in ['build', 'dist', '.venv'] if os.path.exists(p)]"
@python -c "import glob, os; [os.remove(f) for f in glob.glob('*.pyd') + glob.glob('*.so')]"
@python -c "import shutil, glob; [shutil.rmtree(p, ignore_errors=True) for p in glob.glob('*.egg-info')]"
info:
@echo "📊 Build info:"
@echo "pybind11 CMake Dir: `uv run python -c "import pybind11; print(pybind11.get_cmake_dir())"`"
@echo "Python Executable: `uv run python -c "import sys; print(sys.executable)"`"
@python -c "import os; print(f'Build dir: {os.path.abspath(\"build\")}')"
@python -c "import sys; print(f'Python: {sys.executable}')"
@python -c "import shutil; print(f'CMake: {\"found\" if shutil.which(\"cmake\") else \"not found\"}')"
profile:
@echo "⏱️ Profiling build time..."
@python -c "import time; start=time.time(); print(f'Starting build at: {time.strftime(\"%H:%M:%S\")}')"
@python -c "import subprocess, time; start=time.time(); subprocess.run(['just', 'rebuild'], check=True, shell=True); print(f'Full rebuild time: {time.time()-start:.2f}s')"
@python -c "import subprocess, time; start=time.time(); subprocess.run(['just', 'dev'], check=True, shell=True); print(f'Incremental dev time: {time.time()-start:.2f}s')"
main.cpp:
#include <pybind11/pybind11.h>
#include <string>
// Define the MACRO_STRINGIFY macro - handle empty values
#define STRINGIFY_HELPER(x) #x
#define STRINGIFY(x) STRINGIFY_HELPER(x)
std::string hello_world() {
return "Hello, World from C++!";
}
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
namespace py = pybind11;
PYBIND11_MODULE(hello_pybind11, m) {
m.doc() = "pybind11 hello world plugin";
m.def("hello_world", &hello_world, "A function that returns a greeting");
m.def("add", &add, "A function that adds two numbers");
m.def("sub", &sub, "A function that subtracts two numbers");
// Set version - handle empty VERSION_INFO gracefully
#if defined(VERSION_INFO)
m.attr("__version__") = STRINGIFY(VERSION_INFO);
#else
m.attr("__version__") = "dev";
#endif
}
pyproject.toml:
[build-system]
requires = ["scikit-build-core", "pybind11"]
build-backend = "scikit_build_core.build"
[project]
name = "hello-pybind11"
version = "0.1.0"
description = "A simple hello world example using pybind11"
authors = [{name = "Your Name", email = "[email protected]"}]
dependencies = []
requires-python = ">=3.8"
[tool.scikit-build]
# Use Release builds for better performance
cmake.build-type = "Release"
build.verbose = true # Enable verbose output for debugging
wheel.expand-macos-universal-tags = true
build-dir = "build"
[tool.scikit-build.cmake.define]
PYBIND11_FINDPYTHON = "ON"
test.py:
#!/usr/bin/env python3
"""Test script for hello_pybind11 module"""
try:
import hello_pybind11
print("Testing hello_pybind11 module...")
print(f"hello_world(): {hello_pybind11.hello_world()}")
print(f"add(5, 3): {hello_pybind11.add(5, 3)}")
print(f"sub(5, 3): {hello_pybind11.sub(5, 3)}")
print(f"Version: {hello_pybind11.__version__}")
print("All tests passed!")
except ImportError as e:
print(f"Failed to import hello_pybind11: {e}")
print("Make sure to build and install the module first")
except Exception as e:
print(f"Error: {e}")
just setup
, just clean
, just rebuild
recipes are ok... but the overhead and iteration times are quite slow, specially if the bootstrapping has already been made, for instance, trying to run just rebuild check
it takes
ptime just rebuild check
🔄 Full rebuild...
←[1muv pip install -e . --config-settings=build-dir=build --force-reinstall --no-deps←[0m
Resolved 1 package in 4ms
Built hello-pybind11 @ file:///D:/example
Prepared 1 package in 7.59s
Uninstalled 1 package in 1ms
Installed 1 package in 6ms
~ hello-pybind11==0.1.0 (from file:///D:/example)
⚡ Quick import test...
Hello, World from C++!
5 + 3 = 8
Execution time: 8.266 s
The current workflow feels unnecessarily slow for such a simple project—especially considering the source code hasn't even changed. Ideally, using CMake with Ninja would result in near-instant builds. I've attempted to streamline things by creating a couple of extra pre-dev and dev recipes, but they aren't working reliably. For example, when running tests, it's still using an outdated .pyd module.
My question is: what's the recommended, fastest way to achieve an optimal edit-compile-test cycle in a cross-platform manner? I'm using uv and just specifically to keep things portable, and I'm wondering if there are best practices or lesser-known features in scikit-build that could help here. Since I'm relatively new to it, I'd really appreciate guidance on how to set this up properly.