Skip to content
This repository has been archived by the owner on Dec 10, 2024. It is now read-only.

Build: Switch from pybind to nanobind #110

Merged
merged 5 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[build-system]
# using scikit-build-core>0.9.3 disables editable mode
requires = ["hatchling", "scikit-build-core==0.9.2", "pybind11~=2.11.1"]
requires = ["hatchling", "scikit-build-core==0.9.2", "nanobind~=2.2.0"]
build-backend = "hatchling.build"

[project]
Expand Down Expand Up @@ -52,9 +52,9 @@ dev = [
"pytest-xdist~=3.6.1",
"dash~=2.18.1",
"dash_cytoscape~=1.0.2",
"pybind11~=2.11.1",
"nanobind~=2.2.0",
]
test = ["pytest>=7.1.3,<9.0.0", "pytest-xdist~=3.6.1", "pybind11~=2.11.1"]
test = ["pytest>=7.1.3,<9.0.0", "pytest-xdist~=3.6.1", "nanobind~=2.2.0"]


[project.scripts]
Expand Down
44 changes: 34 additions & 10 deletions src/faebryk/core/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,55 @@ if(SKBUILD)
set(Python_INCLUDE_DIR "${PYTHON_INCLUDE_DIR}")
set(Python_LIBRARY "${PYTHON_LIBRARY}")
endif()
set(PYBIND11_FINDPYTHON OFF)
find_package(Python COMPONENTS Interpreter Development.Module)
find_package(pybind11 CONFIG REQUIRED)
if (CMAKE_VERSION VERSION_LESS 3.18)
set(DEV_MODULE Development)
else()
set(DEV_MODULE Development.Module)
endif()
find_package(Python COMPONENTS Interpreter ${DEV_MODULE} REQUIRED)
execute_process(
COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT)
find_package(nanobind CONFIG REQUIRED)

message(STATUS "Python_EXECUTABLE: ${Python_EXECUTABLE}")
message(STATUS "Python_INCLUDE_DIR: ${Python_INCLUDE_DIR}")
message(STATUS "Python_LIBRARY: ${Python_LIBRARY}")

if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()

# configure ------------------------------------------------------------
# c++ standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# turn on optimization
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")
# enable debug symbols
if(${EDITABLE})
set(CMAKE_BUILD_TYPE Debug)
else()
set(CMAKE_BUILD_TYPE Release)
endif()

# if editable and GCC enable colors
if(${EDITABLE} AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always")
endif()
# source files ---------------------------------------------------------
include_directories(${CMAKE_SOURCE_DIR}/include)
set(SOURCE_FILES src/main.cpp)

# build ----------------------------------------------------------------
pybind11_add_module(${PROJECT_NAME} ${SOURCE_FILES})
nanobind_add_module(${PROJECT_NAME} ${SOURCE_FILES})

install(TARGETS ${PROJECT_NAME} DESTINATION .)

if(${EDITABLE})
# create pyi stub file with type annotations
nanobind_add_stub(${PROJECT_NAME}_stub
MODULE ${PROJECT_NAME}
OUTPUT ${PROJECT_NAME}.pyi
PYTHON_PATH $<TARGET_FILE_DIR:${PROJECT_NAME}>
MARKER_FILE py.typed
DEPENDS ${PROJECT_NAME})

#TODO currently pyi is imported into source dir by editable __init__ load
# better to do that automatically with a precommit hook or in the CI
endif()
71 changes: 41 additions & 30 deletions src/faebryk/core/cpp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@

import json
import logging
import pathlib
import shutil
from importlib.metadata import Distribution
from typing import Callable

logger = logging.getLogger(__name__)

Expand All @@ -15,7 +12,7 @@
def is_editable_install():
distro = Distribution.from_name("faebryk")
return (
json.loads(distro.read_text("direct_url.json"))
json.loads(distro.read_text("direct_url.json") or "")
.get("dir_info", {})
.get("editable", False)
)
Expand All @@ -26,24 +23,30 @@ def compile_and_load():
Forces C++ to compile into faebryk_core_cpp_editable module which is then loaded
into _cpp.
"""
import os
import platform
import shutil
import sys
from pathlib import Path

from faebryk.libs.header import formatted_file_contents, get_header
from faebryk.libs.util import run_live

cpp_dir = pathlib.Path(__file__).parent
build_dir = cpp_dir / "build"
_thisfile = Path(__file__)
_thisdir = _thisfile.parent
_cmake_dir = _thisdir
_build_dir = _cmake_dir / "build"
pyi_source = _build_dir / "faebryk_core_cpp_editable.pyi"

date_files = [pyi_source]
dates = {k: os.path.getmtime(k) if k.exists() else 0 for k in date_files}

# check for cmake binary existing
if not shutil.which("cmake"):
raise RuntimeError(
"cmake not found, needed for compiling c++ code in editable mode"
)

pybind11_dir = run_live(
[sys.executable, "-m", "pybind11", "--cmakedir"], logger=logger
).strip()

# Force recompile
# subprocess.run(["rm", "-rf", str(build_dir)], check=True)

Expand All @@ -59,44 +62,52 @@ def compile_and_load():
[
"cmake",
"-S",
str(cpp_dir),
str(_cmake_dir),
"-B",
str(build_dir),
str(_build_dir),
"-DEDITABLE=1",
f"-DCMAKE_PREFIX_PATH={pybind11_dir}",
"-DPython_EXECUTABLE=" + sys.executable,
]
+ other_flags,
*other_flags,
],
logger=logger,
)
run_live(
[
"cmake",
"--build",
str(build_dir),
str(_build_dir),
],
logger=logger,
)

if not build_dir.exists():
if not _build_dir.exists():
raise RuntimeError("build directory not found")

sys.path.append(str(build_dir))
global _cpp
import faebryk_core_cpp_editable as _cpp # type: ignore
# add build dir to sys path
sys.path.append(str(_build_dir))

modified = {k for k, v in dates.items() if os.path.getmtime(k) > v}

# move autogenerated type stub file to source directory
if pyi_source in modified:
pyi_out = _thisfile.with_suffix(".pyi")
pyi_out.write_text(
formatted_file_contents(
get_header()
+ "\n"
+ "# This file is auto-generated by nanobind.\n"
+ "# Do not edit this file directly; edit the corresponding\n"
+ "# C++ file instead."
+ pyi_source.read_text(),
is_pyi=True,
)
)


# Re-export c++ with type hints provided by __init__.pyi
if is_editable_install():
logger.warning("faebryk is installed as editable package, compiling c++ code")
compile_and_load()
from faebryk_core_cpp_editable import * # type: ignore # noqa: E402, F403
else:
# check whether module is available
try:
import faebryk_core_cpp as _cpp # type: ignore # noqa: E402
except ImportError:
logger.warning("faebryk_core_cpp module not found, assuming editable mode")
compile_and_load()


# Re-export c++ with type hints
add: Callable[[int, int], int] = _cpp.add
from faebryk_core_cpp import * # type: ignore # noqa: E402, F403
9 changes: 9 additions & 0 deletions src/faebryk/core/cpp/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This file is part of the faebryk project
# SPDX-License-Identifier: MIT

# This file is auto-generated by nanobind.
# Do not edit this file directly; edit the corresponding
# C++ file instead.

def add(arg0: int, arg1: int, /) -> int:
"""A function that adds two numbers"""
18 changes: 10 additions & 8 deletions src/faebryk/core/cpp/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,28 @@
* SPDX-License-Identifier: MIT
*/

#include <pybind11/pybind11.h>
#include <nanobind/nanobind.h>

// check if c++20 is used
#if __cplusplus < 202002L
#error "C++20 is required"
#endif

namespace py = pybind11;

int add(int i, int j) {
return i + j;
}
namespace nb = nanobind;

#if EDITABLE
#define PYMOD(m) PYBIND11_MODULE(faebryk_core_cpp_editable, m)
#define PYMOD(m) NB_MODULE(faebryk_core_cpp_editable, m)
#warning "EDITABLE"
#else
#define PYMOD(m) PYBIND11_MODULE(faebryk_core_cpp, m)
#define PYMOD(m) NB_MODULE(faebryk_core_cpp, m)
#endif

// -------------------------------------------------------------------------------------

int add(int i, int j) {
return i + j;
}

PYMOD(m) {
m.doc() = "faebryk core c++ module";

Expand Down
21 changes: 21 additions & 0 deletions src/faebryk/libs/header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# This file is part of the faebryk project
# SPDX-License-Identifier: MIT


import black


def get_header():
return (
"# This file is part of the faebryk project\n"
"# SPDX-License-Identifier: MIT\n"
)


def formatted_file_contents(file_contents: str, is_pyi: bool = False) -> str:
return black.format_str(
file_contents,
mode=black.Mode(
is_pyi=is_pyi,
),
)
4 changes: 2 additions & 2 deletions src/faebryk/libs/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -1092,7 +1092,7 @@ def run_live(
stdout_level: int | None = logging.DEBUG,
stderr_level: int | None = logging.ERROR,
**kwargs,
) -> str:
) -> tuple[str, subprocess.Popen]:
"""Runs a process and logs the output live."""

process = subprocess.Popen(
Expand Down Expand Up @@ -1134,4 +1134,4 @@ def run_live(
process.returncode, args[0], "".join(stdout)
)

return "\n".join(stdout)
return "\n".join(stdout), process