From 63fae939986cd4c2b230b7383ff30d644fc62a60 Mon Sep 17 00:00:00 2001 From: Catherine Date: Wed, 27 May 2026 07:58:18 +0000 Subject: [PATCH] Update top-level Python project for CMake compatibility. This commit reimplements the (no longer recommended) setuptools based build system using a standards-based in-tree PEP517 build backend. The implementation is partially based on https://codeberg.org/ziglang/zig-pypi/src/branch/main/make_wheels.py which is licensed under BSD-0-clause. --- cmake/GetPyosysVersion.cmake | 14 ++++ kernel/yosys.cc | 1 + pyosys/build/local_backend.py | 146 ++++++++++++++++++++++++++++++++++ pyosys/hashlib.h | 3 + pyproject.toml | 7 +- setup.py | 138 -------------------------------- 6 files changed, 168 insertions(+), 141 deletions(-) create mode 100644 cmake/GetPyosysVersion.cmake create mode 100644 pyosys/build/local_backend.py delete mode 100644 setup.py diff --git a/cmake/GetPyosysVersion.cmake b/cmake/GetPyosysVersion.cmake new file mode 100644 index 000000000..ae5122d23 --- /dev/null +++ b/cmake/GetPyosysVersion.cmake @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.27) +set(CMAKE_MESSAGE_LOG_LEVEL ERROR) +set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) +include(YosysVersion) + +yosys_extract_version() +if (YOSYS_VERSION_COMMIT EQUAL "0") + set(yosys_version "${YOSYS_VERSION_MAJOR}.${YOSYS_VERSION_MINOR}") +elseif (YOSYS_VERSION_COMMIT STREQUAL "") + set(yosys_version "${YOSYS_VERSION_MAJOR}.${YOSYS_VERSION_MINOR}.post9999") +else() + set(yosys_version "${YOSYS_VERSION_MAJOR}.${YOSYS_VERSION_MINOR}.post${YOSYS_VERSION_COMMIT}") +endif() +execute_process(COMMAND ${CMAKE_COMMAND} -E echo "${yosys_version}") diff --git a/kernel/yosys.cc b/kernel/yosys.cc index d4e0eec8c..1a0da4774 100644 --- a/kernel/yosys.cc +++ b/kernel/yosys.cc @@ -228,6 +228,7 @@ PYBIND11_MODULE(pyosys, m) { // This should not affect using wheels as the dylib has to actually be called // libyosys_dummy.so for this function to be interacted with at all. PYBIND11_MODULE(libyosys_dummy, _) { + (void)_; throw py::import_error("Change your import from 'import libyosys' to 'from pyosys import libyosys'."); } #endif diff --git a/pyosys/build/local_backend.py b/pyosys/build/local_backend.py new file mode 100644 index 000000000..e02cd6655 --- /dev/null +++ b/pyosys/build/local_backend.py @@ -0,0 +1,146 @@ +# To build a wheel with additional CMake options, use `--build-option`, e.g.: +# +# python -m build -w -Ccmake=-DYOSYS_COMPILER_LAUNCHER=ccache +# pip install -Ccmake=-DYOSYS_COMPILER_LAUNCHER=ccache . + +import os +import sys +import pathlib +import tarfile +import tempfile +import subprocess +import sysconfig +from email.policy import EmailPolicy +from email.message import EmailMessage +from wheel.wheelfile import WheelFile + + +PROJECT_NAME = "pyosys" +PROJECT_VERSION = subprocess.check_output([ + "cmake", + f"-DCMAKE_SOURCE_DIR={os.getcwd()}", + "-P", "cmake/GetPyosysVersion.cmake" +], encoding="ascii").strip() +DIST_NAME = f"{PROJECT_NAME}-{PROJECT_VERSION}" + +# https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/ +if sys.implementation.name == "cpython": + PYTHON_TAG = f"cp{sysconfig.get_config_var("py_version_nodot")}" +else: + raise NotImplementedError("unsupported Python implementation") +PLATFORM_TAG = sysconfig.get_platform().replace("-", "_") +COMPAT_TAG = f"{PYTHON_TAG}-none-{PLATFORM_TAG}" + + +def compile_pyosys(cmake_options=[], parallel=os.cpu_count() or 1): + install_dir = tempfile.TemporaryDirectory(prefix="pyosys_install") + with tempfile.TemporaryDirectory(prefix="pyosys_build") as build_dir: + subprocess.check_call([ + "cmake", + "-S", ".", + "-B", build_dir, + "-DYOSYS_WITH_PYTHON=ON", + "-DYOSYS_INSTALL_DRIVER=OFF", + "-DYOSYS_INSTALL_LIBRARY=OFF", + "-DYOSYS_INSTALL_PYTHON=ON", + f"-DCMAKE_INSTALL_PREFIX={install_dir.name}", + f"-DYOSYS_INSTALL_PYTHON_SITEDIR=python", + *cmake_options, + ]) + subprocess.check_call([ + "cmake", + "--build", build_dir, + "-t", "pyosys", + f"-j{parallel}", + ]) + subprocess.check_call([ + "cmake", + "--install", build_dir, + "--strip", + ]) + return install_dir + + +def make_message(headers, payload=None): + msg = EmailMessage(policy=EmailPolicy(max_line_length=0)) + for name, value in headers: + if isinstance(value, list): + for value_part in value: + msg[name] = value_part + else: + msg[name] = value + if payload: + msg.set_payload(payload) + return bytes(msg) + + +def build_sdist(sdist_dir, config_settings=None): + sdist_filename = f"{DIST_NAME}.tar.gz" + + with tarfile.open(pathlib.Path(sdist_dir) / sdist_filename, "w:gz", + format=tarfile.PAX_FORMAT) as sdist: + def exclude_build(entry): + name = entry.name.removeprefix(f"{DIST_NAME}/") + if name in (".cache", "build", "dist"): + return + if os.path.basename(name) in (".git", "__pycache__"): + return + return entry + sdist.add(os.getcwd(), arcname=DIST_NAME, filter=exclude_build) + + return sdist_filename + + +def get_metadata_files(): + with open("README.md", "rb") as readme: + long_description = readme.read() + + return { + "WHEEL": make_message([ + ("Wheel-Version", "1.0"), + ("Generator", "pyosys build backend"), + ("Root-Is-Purelib", "false"), + ("Tag", [COMPAT_TAG]), + ]), + "METADATA": make_message([ + ("Metadata-Version", "2.4"), + ("Name", PROJECT_NAME), + ("Version", PROJECT_VERSION), + ("Summary", "Python access to libyosys"), + ("Description-Content-Type", "text/markdown"), + ("License-Expression", "MIT"), + ("Classifier", "Programming Language :: Python :: 3"), + ("Classifier", "Intended Audience :: Developers"), + ("Classifier", "Operating System :: POSIX :: Linux"), + ("Classifier", "Operating System :: MacOS :: MacOS X"), + ("Requires-Python", ">=3.8"), + ], long_description) + } + + +def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): + os.mkdir(f"{metadata_directory}/{DIST_NAME}.dist-info") + + for filename, contents in get_metadata_files().items(): + with open(f"{metadata_directory}/{DIST_NAME}.dist-info/{filename}", "wb") as f: + f.write(contents) + + return f"{DIST_NAME}.dist-info" + + +def build_wheel(wheel_dir, config_settings=None, metadata_directory=None): + wheel_filename = f"{DIST_NAME}-{COMPAT_TAG}.whl" + + with WheelFile(pathlib.Path(wheel_dir) / wheel_filename, "w") as wheel: + for filename, contents in get_metadata_files().items(): + wheel.writestr(f"{DIST_NAME}.dist-info/{filename}", contents) + + cmake_options = [] + if config_settings is not None: + if cmake_options := config_settings.get("cmake"): + if isinstance(cmake_options, str): + cmake_options = [cmake_options] + with compile_pyosys(cmake_options) as install_dir: + wheel.write_files(pathlib.Path(install_dir) / "python") + + return wheel_filename diff --git a/pyosys/hashlib.h b/pyosys/hashlib.h index 386f1c0d8..016611146 100644 --- a/pyosys/hashlib.h +++ b/pyosys/hashlib.h @@ -482,9 +482,11 @@ void bind_idict(module &m, const char *name_cstr) { return make_iterator(s.begin(), s.end()); }) .def("values", [](args _){ + (void)_; throw type_error("idicts do not support iteration on the integers"); }) .def("items", [](args _){ + (void)_; throw type_error("idicts do not support pairwise iteration"); }) .def("update", [](C &s, iterable other) { @@ -521,6 +523,7 @@ void bind_idict(module &m, const char *name_cstr) { for (const char *mutator: {"__setitem__", "__delitem__", "pop", "popitem", "setdefault"}) { cls.def(mutator, [](args _) { + (void)_; throw type_error("idicts do not support arbitrary element mutation"); }); } diff --git a/pyproject.toml b/pyproject.toml index 8893137d8..37d3765fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,11 @@ [build-system] requires = [ - "setuptools>=42", + "wheel", "pybind11>=3,<4", - "cxxheaderparser" + "cxxheaderparser", ] -build-backend = "setuptools.build_meta" +backend-path = ["pyosys/build"] +build-backend = "local_backend" [tool.ruff] target-version = "py38" diff --git a/setup.py b/setup.py deleted file mode 100644 index 8b786cb32..000000000 --- a/setup.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (C) 2024 Efabless Corporation -# -# Permission to use, copy, modify, and/or distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -import os -import re -import shlex -import shutil -from pathlib import Path -from setuptools import setup, Extension - -import pybind11 -from pybind11.setup_helpers import build_ext - -__yosys_root__ = Path(__file__).parent - -yosys_version_rx = re.compile(r"YOSYS_VER\s*:=\s*([\w\-\+\.]+)") - -with open(__yosys_root__ / "Makefile", encoding="utf8") as f: - # Extract and convert + to patch version - version = yosys_version_rx.search(f.read())[1].replace("+", ".") - - -class libyosys_so_ext(Extension): - def __init__( - self, - ) -> None: - super().__init__( - "libyosys.so", - [], - ) - - # when iterating locally, you probably want to set this variable - # to avoid mass rebuilds bec of pybind11's include path changing - pybind_include = os.getenv("_FORCE_PYBIND_INCLUDE", pybind11.get_include()) - - self.args = [ - f"PYBIND11_INCLUDE={pybind_include}", - "ENABLE_PYOSYS=1", - # Would need to be installed separately by the user - "ENABLE_TCL=0", - "ENABLE_READLINE=0", - "ENABLE_EDITLINE=0", - "PYOSYS_USE_UV=0", # + install requires takes its role when building wheels - # Always compile and include ABC in wheel - "ABCEXTERNAL=", - # Show compile commands - "PRETTY=0", - ] - - def custom_build(self, bext: build_ext): - make_flags_split = shlex.split(os.getenv("makeFlags", "")) - # abc linking takes a lot of memory, best get it out of the way first - bext.spawn( - [ - "make", - f"-j{os.cpu_count() or 1}", - "yosys-abc", - *make_flags_split, - *self.args, - ] - ) - # build libyosys and share with abc out of the way - bext.spawn( - [ - "make", - f"-j{os.cpu_count() or 1}", - self.name, - "share", - *make_flags_split, - *self.args, - ] - ) - ext_fullpath = Path(bext.get_ext_fullpath(self.name)) - build_path = ext_fullpath.parents[1] - pyosys_path = build_path / "pyosys" - os.makedirs(pyosys_path, exist_ok=True) - - # libyosys.so - target = pyosys_path / self.name - shutil.copy(self.name, target) - bext.spawn(["strip", "-S", str(target)]) - - # yosys-abc - yosys_abc_target = pyosys_path / "yosys-abc" - shutil.copy("yosys-abc", yosys_abc_target) - bext.spawn(["strip", "-S", str(yosys_abc_target)]) - - # share directory - share_target = pyosys_path / "share" - try: - shutil.rmtree(share_target) - except FileNotFoundError: - pass - - shutil.copytree("share", share_target) - - -class custom_build_ext(build_ext): - def build_extension(self, ext) -> None: - if not hasattr(ext, "custom_build"): - return super().build_extension(ext) - return ext.custom_build(self) - - -with open(__yosys_root__ / "README.md", encoding="utf8") as f: - long_description = f.read() - -setup( - name="pyosys", - packages=["pyosys"], - version=version, - description="Python access to libyosys", - long_description=long_description, - long_description_content_type="text/markdown", - license="MIT", - classifiers=[ - "Programming Language :: Python :: 3", - "Intended Audience :: Developers", - "Operating System :: POSIX :: Linux", - "Operating System :: MacOS :: MacOS X", - ], - python_requires=">=3.8", - ext_modules=[libyosys_so_ext()], - cmdclass={ - "build_ext": custom_build_ext, - }, -)