diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 8139b5af5..e6381a637 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -36,13 +36,11 @@ jobs: runner: "macos-15", archs: "arm64", }, - ## Windows is disabled because of an issue with compiling FFI as - ## under MinGW in the GitHub Actions environment (SHELL variable has - ## whitespace.) + ## Windows still needs to be tested. # { - # name: "Windows Server 2019", + # name: "Windows Server 2025", # family: "windows", - # runner: "windows-2019", + # runner: "windows-2025", # archs: "AMD64", # }, ] @@ -77,17 +75,14 @@ jobs: name: "[Windows] Flex/Bison" run: | choco install winflexbison3 - - if: ${{ matrix.os.family == 'macos' && matrix.os.archs == 'arm64' }} - name: "[macOS/arm64] Install Python 3.8 (see: https://cibuildwheel.pypa.io/en/stable/faq/#macos-building-cpython-38-wheels-on-arm64)" - uses: actions/setup-python@v5 - with: - python-version: 3.8 - name: Build wheels - uses: pypa/cibuildwheel@v2.21.1 + uses: pypa/cibuildwheel@v3.4.1 env: # * APIs not supported by PyPy # * Musllinux disabled because it increases build time from 48m to ~3h CIBW_SKIP: > + cp38* + cp39* pp* *musllinux* CIBW_ARCHS: ${{ matrix.os.archs }} @@ -104,7 +99,6 @@ jobs: OPTFLAGS=-O3 PKG_CONFIG_PATH=./ffi/pfx/lib/pkgconfig MACOSX_DEPLOYMENT_TARGET=11 - makeFlags='CONFIG=clang' PATH="$PWD/bison/src:$PATH" CIBW_BEFORE_BUILD: bash ./.github/workflows/wheels/cibw_before_build.sh CIBW_TEST_COMMAND: python3 {project}/tests/pyosys/run_tests.py diff --git a/.github/workflows/wheels/_run_cibw_linux.py b/.github/workflows/wheels/_run_cibw_linux.py deleted file mode 100644 index 1e8a0f497..000000000 --- a/.github/workflows/wheels/_run_cibw_linux.py +++ /dev/null @@ -1,52 +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. -""" -This runs the cibuildwheel step from the wheels workflow locally. -""" - -import os -import yaml -import platform -import subprocess -from pathlib import Path - -__yosys_root__ = Path(__file__).absolute().parents[3] - -for source in ["ffi", "bison"]: - if not (__yosys_root__ / source).is_dir(): - print( - "You need to download ffi and bison in a similar manner to wheels.yml first." - ) - exit(-1) - -with open(__yosys_root__ / ".github" / "workflows" / "wheels.yml") as f: - workflow = yaml.safe_load(f) - -env = os.environ.copy() - -steps = workflow["jobs"]["build_wheels"]["steps"] -cibw_step = None -for step in steps: - if (step.get("uses") or "").startswith("pypa/cibuildwheel"): - cibw_step = step - break - -for key, value in cibw_step["env"].items(): - if key.endswith("WIN") or key.endswith("MAC"): - continue - env[key] = value - -env["CIBW_ARCHS"] = os.getenv("CIBW_ARCHS", platform.machine()) -subprocess.check_call(["cibuildwheel"], env=env) diff --git a/.github/workflows/wheels/cibw_before_build.sh b/.github/workflows/wheels/cibw_before_build.sh index 1ce96b291..217cb8875 100644 --- a/.github/workflows/wheels/cibw_before_build.sh +++ b/.github/workflows/wheels/cibw_before_build.sh @@ -1,9 +1,6 @@ set -e set -x -# Don't use Python objects from previous compiles -make clean-py - # DEBUG: show python3 and python3-config outputs if [ "$(uname)" != "Linux" ]; then # https://github.com/pypa/cibuildwheel/issues/2021 diff --git a/CMakeLists.txt b/CMakeLists.txt index 89e60e31a..287631547 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,6 +62,13 @@ cmake_dependent_option(YOSYS_INSTALL_PYTHON "Install Python extension module" OF YOSYS_WITH_PYTHON OFF) set(YOSYS_INSTALL_PYTHON_SITEDIR "" CACHE STRING "Path to Python package installation directory") +# This option is something of a hack to make Python wheels buildable in an environment that has +# the `Development.Module` component, but not `Development.Embed` (e.g. cibuildwheel). It is only +# present to be used in the wheel build and is not supported otherwise. +cmake_dependent_option(YOSYS_BUILD_PYTHON_ONLY "Build only Pyosys components" ON + "NOT (YOSYS_INSTALL_DRIVER OR YOSYS_INSTALL_LIBRARY) AND YOSYS_INSTALL_PYTHON" OFF) +mark_as_advanced(YOSYS_BUILD_PYTHON_ONLY) + # Configure compiler. set(CMAKE_EXPORT_COMPILE_COMMANDS YES) @@ -253,9 +260,9 @@ if (tcl_FOUND) endif() if (YOSYS_WITH_PYTHON) - find_package(Python3Embed REQUIRED) - set_property(GLOBAL PROPERTY _CMAKE_Python3Embed_REQUIRED_VERSION "== ${Python3_VERSION}") - set_package_properties(Python3Embed PROPERTIES + find_package(Python3Devel REQUIRED) + set_property(GLOBAL PROPERTY _CMAKE_Python3Devel_REQUIRED_VERSION "== ${Python3_VERSION}") + set_package_properties(Python3Devel PROPERTIES URL "https://www.python.org/" DESCRIPTION "Dynamic programming language (Embedding)" PURPOSE "Binding Yosys API" @@ -288,7 +295,7 @@ condition(YOSYS_ENABLE_LIBFFI Dlfcn_FOUND AND libffi_FOUND AND NOT YOSYS_WITHOUT condition(YOSYS_ENABLE_READLINE readline_FOUND AND NOT YOSYS_WITHOUT_READLINE) condition(YOSYS_ENABLE_EDITLINE editline_FOUND AND NOT YOSYS_WITHOUT_EDITLINE AND NOT YOSYS_ENABLE_READLINE) condition(YOSYS_ENABLE_TCL tcl_FOUND AND libtommath_FOUND AND NOT YOSYS_WITHOUT_TCL) -condition(YOSYS_ENABLE_PYTHON Python3Embed_FOUND AND PyosysEnv_FOUND AND YOSYS_WITH_PYTHON) +condition(YOSYS_ENABLE_PYTHON Python3Devel_FOUND AND PyosysEnv_FOUND AND YOSYS_WITH_PYTHON) condition(YOSYS_ENABLE_VERIFIC YOSYS_VERIFIC_DIR AND zlib_FOUND) # Describe dependencies and features @@ -391,46 +398,52 @@ endif() # Compute a transitive closure of enabled components. yosys_expand_components(library_components essentials ${YOSYS_COMPONENTS}) -yosys_expand_components(driver_components driver ${YOSYS_COMPONENTS}) +if (NOT YOSYS_BUILD_PYTHON_ONLY) + yosys_expand_components(driver_components driver ${YOSYS_COMPONENTS}) +endif() # Main Yosys executable (compiler driver). -yosys_cxx_executable(yosys - OUTPUT_NAME yosys - INSTALL_IF ${YOSYS_INSTALL_DRIVER} -) -yosys_link_components(yosys PRIVATE ${driver_components}) -set_property(TARGET yosys PROPERTY ENABLE_EXPORTS ON) -if (MINGW) - target_link_options(yosys PRIVATE LINKER:--export-all-symbols) - set_target_properties(yosys PROPERTIES - # Final name: `yosys.exe.a` (linked to explicitly) - IMPORT_PREFIX "" - IMPORT_SUFFIX ".exe.a" +if (NOT YOSYS_BUILD_PYTHON_ONLY) + yosys_cxx_executable(yosys + OUTPUT_NAME yosys + INSTALL_IF ${YOSYS_INSTALL_DRIVER} ) - if (YOSYS_INSTALL_DRIVER) - install(FILES ${CMAKE_BINARY_DIR}/yosys.exe.a DESTINATION ${YOSYS_INSTALL_LIBDIR}) + yosys_link_components(yosys PRIVATE ${driver_components}) + set_property(TARGET yosys PROPERTY ENABLE_EXPORTS ON) + if (MINGW) + target_link_options(yosys PRIVATE LINKER:--export-all-symbols) + set_target_properties(yosys PROPERTIES + # Final name: `yosys.exe.a` (linked to explicitly) + IMPORT_PREFIX "" + IMPORT_SUFFIX ".exe.a" + ) + if (YOSYS_INSTALL_DRIVER) + install(FILES ${CMAKE_BINARY_DIR}/yosys.exe.a DESTINATION ${YOSYS_INSTALL_LIBDIR}) + endif() endif() -endif() -target_compile_options(yosys PRIVATE -fsanitize=undefined) + target_compile_options(yosys PRIVATE -fsanitize=undefined) +endif() # Yosys components as a library. -if (BUILD_SHARED_LIBS) - set(libyosys_type SHARED) -else() - set(libyosys_type STATIC) -endif() -yosys_cxx_library(libyosys ${libyosys_type} - OUTPUT_NAME libyosys - INSTALL_IF ${YOSYS_INSTALL_LIBRARY} -) -yosys_link_components(libyosys PRIVATE ${library_components}) -add_library(Yosys::libyosys ALIAS libyosys) -if (MINGW) - set_target_properties(libyosys PROPERTIES - # Final name: `libyosys.dll.a` (linked to via `-lyosys`) - IMPORT_PREFIX "" +if (NOT YOSYS_BUILD_PYTHON_ONLY) + if (BUILD_SHARED_LIBS) + set(libyosys_type SHARED) + else() + set(libyosys_type STATIC) + endif() + yosys_cxx_library(libyosys ${libyosys_type} + OUTPUT_NAME libyosys + INSTALL_IF ${YOSYS_INSTALL_LIBRARY} ) + yosys_link_components(libyosys PRIVATE ${library_components}) + add_library(Yosys::libyosys ALIAS libyosys) + if (MINGW) + set_target_properties(libyosys PROPERTIES + # Final name: `libyosys.dll.a` (linked to via `-lyosys`) + IMPORT_PREFIX "" + ) + endif() endif() # Yosys data files (mainly headers and technological libraries). @@ -500,37 +513,39 @@ set(makefile_depends $<$:yosys-witness> ) -# Tests. -add_subdirectory(tests/unit) +if (NOT YOSYS_BUILD_PYTHON_ONLY) + # Tests. + add_subdirectory(tests/unit) -add_custom_target(test-unit - COMMAND ${CMAKE_CTEST_COMMAND} --test-dir tests/unit --output-on-failure - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} -) - -add_custom_target(test-vanilla - COMMAND make vanilla-test ${makefile_vars} - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests - DEPENDS ${makefile_depends} -) - -add_custom_target(test - DEPENDS test-unit test-vanilla -) - -# Docs. -add_custom_target(docs-prepare - COMMAND make gen ${makefile_vars} - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/docs - DEPENDS ${makefile_depends} -) -foreach (format html latexpdf) - add_custom_target(docs-${format} - COMMAND make ${format} - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/docs - DEPENDS docs-prepare + add_custom_target(test-unit + COMMAND ${CMAKE_CTEST_COMMAND} --test-dir tests/unit --output-on-failure + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) -endforeach() + + add_custom_target(test-vanilla + COMMAND make vanilla-test ${makefile_vars} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests + DEPENDS ${makefile_depends} + ) + + add_custom_target(test + DEPENDS test-unit test-vanilla + ) + + # Docs. + add_custom_target(docs-prepare + COMMAND make gen ${makefile_vars} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/docs + DEPENDS ${makefile_depends} + ) + foreach (format html latexpdf) + add_custom_target(docs-${format} + COMMAND make ${format} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/docs + DEPENDS docs-prepare + ) + endforeach() +endif() # Utilities. add_custom_target(print-version diff --git a/cmake/FindPyosysEnv.cmake b/cmake/FindPyosysEnv.cmake index be4672ef7..59fd386fc 100644 --- a/cmake/FindPyosysEnv.cmake +++ b/cmake/FindPyosysEnv.cmake @@ -4,8 +4,10 @@ # whether the host interpreter has the necessary dependencies first, and if it # does not, fall back to using `uv`. -foreach (strategy host uv fail) - if (strategy STREQUAL "host") +foreach (strategy virtualenv host uv fail) + if (strategy STREQUAL "virtualenv") + set(PyosysEnv_PYTHON $ENV{VIRTUAL_ENV}/bin/python) + elseif (strategy STREQUAL "host") set(PyosysEnv_PYTHON ${Python3_EXECUTABLE}) elseif (strategy STREQUAL "uv") set(PyosysEnv_PYTHON uv run --no-project --with pybind11>3,<4 --with cxxheaderparser python) diff --git a/cmake/FindPython3Embed.cmake b/cmake/FindPython3Devel.cmake similarity index 56% rename from cmake/FindPython3Embed.cmake rename to cmake/FindPython3Devel.cmake index f3fda9070..b3589a96d 100644 --- a/cmake/FindPython3Embed.cmake +++ b/cmake/FindPython3Devel.cmake @@ -6,10 +6,22 @@ get_property(packages_found GLOBAL PROPERTY PACKAGES_FOUND) get_property(packages_not_found GLOBAL PROPERTY PACKAGES_NOT_FOUND) get_property(required_version GLOBAL PROPERTY _CMAKE_Python3_REQUIRED_VERSION) +# A hack to make pyosys buildable in wheel-only environments. +# `Interpreter` is a part of the component set to ensure that a Python implementation without +# an interpreter that's earlier in the search order won't be selected instead of the desired one. +# (This is awful and should be removed once CMake 4.0 is here.) +if (YOSYS_BUILD_PYTHON_ONLY) + set(components Interpreter Development.Module) +else() + set(components Interpreter Development) +endif() + # The `EXACT` specifier prevents the situation of `FindPython3` discovering a newer libpython-dev # than the interpreter found in the past, rejecting it because it is too new, and giving up. -find_package(Python3 EXACT ${Python3_VERSION} COMPONENTS Development.Embed) -set(Python3Embed_FOUND ${Python3_Development.Embed_FOUND}) +find_package(Python3 EXACT ${Python3_VERSION} COMPONENTS ${components}) +if (Python3_Development.Embed_FOUND OR Python3_Development.Module_FOUND) + set(Python3Devel_FOUND YES) +endif() set_property(GLOBAL PROPERTY PACKAGES_FOUND "${packages_found}") set_property(GLOBAL PROPERTY PACKAGES_NOT_FOUND "${packages_not_found}") 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/CMakeLists.txt b/kernel/CMakeLists.txt index 87b7e5a3c..76b9a9cfa 100644 --- a/kernel/CMakeLists.txt +++ b/kernel/CMakeLists.txt @@ -96,7 +96,7 @@ yosys_core(kernel $<${YOSYS_ENABLE_READLINE}:PkgConfig::readline> $<${YOSYS_ENABLE_EDITLINE}:PkgConfig::editline> $<${YOSYS_ENABLE_TCL}:PkgConfig::tcl> - $<${YOSYS_ENABLE_PYTHON}:Python3::Python> + $<${YOSYS_ENABLE_PYTHON}:Python3::Module> REQUIRES bigint ezsat @@ -172,16 +172,18 @@ yosys_core(fstdata fstdata.h ) -yosys_core(driver - driver.cc - INCLUDE_DIRS - ${pybind11_INCLUDE_DIR} - LIBRARIES - $<${YOSYS_ENABLE_READLINE}:PkgConfig::readline> - $<${YOSYS_ENABLE_EDITLINE}:PkgConfig::editline> - $<${YOSYS_ENABLE_TCL}:PkgConfig::tcl> - $<${YOSYS_ENABLE_PYTHON}:Python3::Python> - REQUIRES - essentials - BOOTSTRAP -) +if (NOT YOSYS_BUILD_PYTHON_ONLY) + yosys_core(driver + driver.cc + INCLUDE_DIRS + ${pybind11_INCLUDE_DIR} + LIBRARIES + $<${YOSYS_ENABLE_READLINE}:PkgConfig::readline> + $<${YOSYS_ENABLE_EDITLINE}:PkgConfig::editline> + $<${YOSYS_ENABLE_TCL}:PkgConfig::tcl> + $<${YOSYS_ENABLE_PYTHON}:Python3::Python> + REQUIRES + essentials + BOOTSTRAP + ) +endif() 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/passes/cmds/CMakeLists.txt b/passes/cmds/CMakeLists.txt index 66b901046..e86ecaa20 100644 --- a/passes/cmds/CMakeLists.txt +++ b/passes/cmds/CMakeLists.txt @@ -115,7 +115,7 @@ yosys_pass(plugin ${pybind11_INCLUDE_DIR} LIBRARIES $<${YOSYS_ENABLE_PLUGINS}:${Dlfcn_LIBRARIES}> - $<${YOSYS_ENABLE_PYTHON}:Python3::Python> + $<${YOSYS_ENABLE_PYTHON}:Python3::Module> ESSENTIAL ) yosys_pass(check diff --git a/pyosys/CMakeLists.txt b/pyosys/CMakeLists.txt index b78eb3277..3cc326451 100644 --- a/pyosys/CMakeLists.txt +++ b/pyosys/CMakeLists.txt @@ -24,6 +24,6 @@ yosys_core(pyosys INCLUDE_DIRS ${pybind11_INCLUDE_DIR} LIBRARIES - $<${YOSYS_ENABLE_PYTHON}:Python3::Python> + $<${YOSYS_ENABLE_PYTHON}:Python3::Module> ESSENTIAL ) diff --git a/pyosys/build/local_backend.py b/pyosys/build/local_backend.py new file mode 100644 index 000000000..8303eefab --- /dev/null +++ b/pyosys/build/local_backend.py @@ -0,0 +1,175 @@ +# 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 re +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')}" + # freethreaded builds have an ABI flag appended, "t" + ABI_TAG = f"cp{sysconfig.get_config_var('py_version_nodot')}{sysconfig.get_config_var('abiflags')}" +else: + raise NotImplementedError("unsupported Python implementation") +# get_platform() always returns the MACOSX_DEPLOYMENT_TARGET this intepreter is +# configured with: +# https://github.com/python/cpython/blob/494f2e3c92cc1b7774cca16fca5c7d1ff18c0de2/Lib/_osx_support.py#L504 +PLATFORM_TAG_RAW = sysconfig.get_platform() +MACOSX_DEPLOYMENT_TARGET_FLAGS = [] +if interpreter_deployment_target := sysconfig.get_config_var("MACOSX_DEPLOYMENT_TARGET"): + cmake_deployment_target = interpreter_deployment_target + interpreter_deployment_target = tuple(int(v) for v in interpreter_deployment_target.split(".")) + # Yosys fails to compile for anything below 10.15 because of std::filesystem + requested_deployment_target = tuple(int(v) for v in os.environ.get("MACOSX_DEPLOYMENT_TARGET", "10.15").split(".")) + if requested_deployment_target > interpreter_deployment_target: + resolved_platform_version_string = ".".join(str(v) for v in requested_deployment_target) + cmake_deployment_target = resolved_platform_version_string + if "." not in resolved_platform_version_string: + # macOS 11+ need to be "bare" for MACOSX_DEPLOYMENT_TARGET but have + # the .0 for Python platform versions + resolved_platform_version_string += ".0" + PLATFORM_TAG_RAW = re.sub(r"(macosx)-\d+\.\d+", rf"\1-{resolved_platform_version_string}", PLATFORM_TAG_RAW) + MACOSX_DEPLOYMENT_TARGET_FLAGS = [f"-DCMAKE_OSX_DEPLOYMENT_TARGET={cmake_deployment_target}"] +# Source for these substitutions: +# https://github.com/pypa/wheel/blob/197012dcb8a9da10570d6486bc1a70305861e7f2/src/wheel/_bdist_wheel.py#L351 +PLATFORM_TAG = PLATFORM_TAG_RAW.lower().replace("-", "_").replace(".", "_").replace(" ", "_") +COMPAT_TAG = f"{PYTHON_TAG}-{ABI_TAG}-{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, + "-DCMAKE_BUILD_TYPE=Release", + f"-DPython3_EXECUTABLE={sys.executable}", + "-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", + "-DYOSYS_BUILD_PYTHON_ONLY=ON", + *cmake_options, + *MACOSX_DEPLOYMENT_TARGET_FLAGS, + ]) + 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.10"), + ], 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", cmake_options): + 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..8e5913a5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,13 @@ [build-system] requires = [ - "setuptools>=42", + "wheel", + "packaging", "pybind11>=3,<4", - "cxxheaderparser" + "cxxheaderparser", ] -build-backend = "setuptools.build_meta" +backend-path = ["pyosys/build"] +build-backend = "local_backend" [tool.ruff] -target-version = "py38" +target-version = "py310" lint.ignore = ["F541"] 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, - }, -)