diff --git a/.github/workflows/wheel.yml b/.github/workflows/wheel.yml index 0fd6174..9e82f58 100644 --- a/.github/workflows/wheel.yml +++ b/.github/workflows/wheel.yml @@ -12,13 +12,15 @@ on: jobs: build: runs-on: ubuntu-latest - container: python:${{ matrix.python-version }}-bullseye + container: python:${{ matrix.python-version }}${{ matrix.image-suffix }}-bullseye strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] - + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + include: + - python-version: '3.13' + image-suffix: '-rc' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build shell: bash run: | @@ -27,33 +29,36 @@ jobs: python -m pip install -U pip build python -m build . - name: Upload Wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: py_build_cmake-whl-${{ matrix.python-version }} path: dist/py_build_cmake-*.whl - name: Upload source - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: py_build_cmake-src-${{ matrix.python-version }} - path: dist/py-build-cmake-*.tar.gz + path: dist/py_build_cmake-*.tar.gz test: needs: build runs-on: ubuntu-latest - container: python:${{ matrix.python-version }}-bullseye + container: python:${{ matrix.python-version }}${{ matrix.image-suffix }}-bullseye strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + include: + - python-version: '3.13' + image-suffix: '-rc' steps: - - uses: actions/checkout@v3 - - uses: actions/download-artifact@v3 + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 with: name: py_build_cmake-whl-${{ matrix.python-version }} path: dist - name: Run Nox run: | - python -m pip install -U pip nox - python -m nox + python -m pip install -U pip nox distlib jinja2 + python -m nox --force-color env: PY_BUILD_CMAKE_WHEEL_DIR: ${{ github.workspace }}/dist @@ -61,14 +66,14 @@ jobs: runs-on: windows-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] - + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 name: Install Python - with: + with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Build shell: powershell run: | @@ -79,51 +84,51 @@ jobs: env: PYTHONUTF8: 1 - name: Upload Wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: py_build_cmake-whl-win-${{ matrix.python-version }} path: dist/py_build_cmake-*.whl - - name: Upload source - uses: actions/upload-artifact@v3 - with: - name: py_build_cmake-src-${{ matrix.python-version }} - path: dist/py-build-cmake-*.tar.gz test-windows: needs: build-windows runs-on: windows-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 name: Install Python - with: + with: python-version: ${{ matrix.python-version }} - - uses: actions/download-artifact@v3 + allow-prereleases: true + - uses: actions/download-artifact@v4 with: name: py_build_cmake-whl-win-${{ matrix.python-version }} path: dist - name: Run Nox run: | - python -m pip install -U pip nox - python -m nox + python -m pip install -U pip nox distlib jinja2 + python -m nox --force-color env: PY_BUILD_CMAKE_WHEEL_DIR: ${{ github.workspace }}/dist build-macos: - runs-on: macos-latest + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] - + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + os: ['macos-13', 'macos-latest'] + exclude: + - python-version: '3.7' + os: 'macos-latest' steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 name: Install Python - with: + with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Build run: | python -m venv py-venv @@ -131,50 +136,81 @@ jobs: python -m pip install -U pip build python -m build . - name: Upload Wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: py_build_cmake-whl-mac-${{ matrix.python-version }} + name: py_build_cmake-whl-${{ matrix.os }}-${{ matrix.python-version }} path: dist/py_build_cmake-*.whl - - name: Upload source - uses: actions/upload-artifact@v3 - with: - name: py_build_cmake-src-${{ matrix.python-version }} - path: dist/py-build-cmake-*.tar.gz test-macos: needs: build-macos - runs-on: macos-latest + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + os: ['macos-13', 'macos-latest'] + exclude: + - python-version: '3.7' + os: 'macos-latest' steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 name: Install Python - with: + with: python-version: ${{ matrix.python-version }} - - uses: actions/download-artifact@v3 + allow-prereleases: true + - uses: actions/download-artifact@v4 with: - name: py_build_cmake-whl-mac-${{ matrix.python-version }} + name: py_build_cmake-whl-${{ matrix.os }}-${{ matrix.python-version }} path: dist - name: Run Nox run: | - python -m pip install -U pip nox - python -m nox + python -m pip install -U pip nox distlib jinja2 + python -m nox --force-color env: PY_BUILD_CMAKE_WHEEL_DIR: ${{ github.workspace }}/dist - release: + cibw: + needs: [build] + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-20.04, windows-2019, macos-13, macos-latest] + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: py_build_cmake-whl-3.11 + path: dist + - name: Build Wheels + uses: pypa/cibuildwheel@7940a4c0e76eb2030e473a5f864f291f63ee879b + with: + package-dir: examples/pybind11-project + output-dir: dist + env: + CIBW_ARCHS_LINUX: 'x86_64 i686' + CIBW_ARCHS_WINDOWS: 'AMD64 x86 ARM64' + CIBW_ARCHS_MACOS: 'x86_64 universal2 arm64' + CIBW_BUILD: 'cp311-* pp39-*' + CIBW_ENVIRONMENT: 'PIP_FIND_LINKS=${{ github.workspace }}/dist PY_BUILD_CMAKE_VERBOSE=1' + CIBW_ENVIRONMENT_LINUX: 'PIP_FIND_LINKS=/project/dist PY_BUILD_CMAKE_VERBOSE=1' + CIBW_ENVIRONMENT_WINDOWS: 'PIP_FIND_LINKS=D:/a/py-build-cmake/py-build-cmake/dist PY_BUILD_CMAKE_VERBOSE=1' + - name: Upload Wheels + uses: actions/upload-artifact@v4 + with: + name: pybind11_project-${{ matrix.os }}-whl + path: dist/*.whl + + check-release: if: ${{ github.event.action == 'released' || github.event.action == 'prereleased' }} - needs: [test, test-windows, test-macos] + needs: [test, test-windows, test-macos, cibw] runs-on: ubuntu-latest container: python:${{ matrix.python-version }}-bullseye strategy: matrix: - python-version: ['3.10'] + python-version: ['3.11'] steps: - - uses: actions/checkout@v3 - - uses: actions/download-artifact@v3 + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 with: name: py_build_cmake-whl-${{ matrix.python-version }} path: dist @@ -184,19 +220,24 @@ jobs: run: | [ "${{ github.event.release.tag_name }}" == "$(python -c 'from importlib.metadata import version as v; print(v("py_build_cmake"))')" ] shell: bash - - name: Install Twine - run: pip install twine - - name: Upload Wheel to PyPI - run: python -m twine upload dist/py_build_cmake-*.whl - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PyPI }} - - uses: actions/download-artifact@v3 + + release: + needs: [check-release] + if: ${{ github.event.action == 'released' || github.event.action == 'prereleased' }} + runs-on: ubuntu-latest + environment: + name: release + url: https://pypi.org/p/py-build-cmake + permissions: + id-token: write # mandatory for trusted publishing + steps: + - uses: actions/download-artifact@v4 with: - name: py_build_cmake-src-${{ matrix.python-version }} + name: py_build_cmake-whl-3.11 path: dist - - name: Upload source to PyPI - run: python -m twine upload dist/py-build-cmake-*.tar.gz - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PyPI }} + - uses: actions/download-artifact@v4 + with: + name: py_build_cmake-src-3.11 + path: dist + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc diff --git a/.gitignore b/.gitignore index 0db9073..1ad5771 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ -py*-venv -.venv +/toolchains +.venv* build .build .*cache __pycache__ *.pyc dist +dist-nox .nox -old \ No newline at end of file +old diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..61661e1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,48 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.3.4" + hooks: + - id: ruff + args: ["--fix", "--show-fixes"] + - repo: https://github.com/psf/black-pre-commit-mirror + rev: "24.3.0" + hooks: + - id: black + - repo: https://github.com/pre-commit/mirrors-mypy + rev: "v1.9.0" + hooks: + - id: mypy + files: ^src + args: [] + additional_dependencies: + - "distlib~=0.3.5" + - "pyproject-metadata~=0.7.1" + - "tomli>=1.2.3,<3; python_version < '3.11'" + - "lark>=1.1.9,<2" + - "click~=8.1.3" + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: "v18.1.2" + hooks: + - id: clang-format + types_or: [c++, c] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: "v4.5.0" + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + exclude: '(^tests/expected_contents/.*\.txt$)|(^docs/(Components|Config)\.md$)' + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: name-tests-test + args: ["--pytest-test-first"] + - id: requirements-txt-fixer + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + exclude: '^docs/(Components|Config)\.md$' diff --git a/.vscode/settings.json b/.vscode/settings.json index cab54b9..917a55b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { - "python.formatting.provider": "yapf", + "python.formatting.provider": "black", "python.linting.mypyEnabled": true, "cmake.configureEnvironment": { "VIRTUAL_ENV": "${workspaceFolder}/.venv" }, -} \ No newline at end of file +} diff --git a/README.md b/README.md index 979d5be..f7963b6 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ backend for creating Python packages with extensions built using CMake. ## Features - Building and packaging C, C++ or Fortran extension modules for Python using CMake - - Declarative configuration using `pyproject.toml` ([PEP 621](https://www.python.org/dev/peps/pep-0621/)), compatible with - [flit](https://github.com/pypa/flit) + - Declarative configuration using `pyproject.toml` ([PEP 621](https://www.python.org/dev/peps/pep-0621/)) - Editable/development installations for Python modules ([PEP 660](https://www.python.org/dev/peps/pep-0660/)) - - Compatible with [pybind11](https://github.com/pybind/pybind11) and [nanobind](https://github.com/wjakob/nanobind) + - Easy integration with [pybind11](https://github.com/pybind/pybind11) and [nanobind](https://github.com/wjakob/nanobind), with stable ABI support - Stub generation for type checking and autocompletion - - Customizable CMake configuration, build and installation options - - Support for multiple installation configurations and components - - Cross-compilation support + - Customizable CMake configuration, build, and installation options + - Support for installation of multiple configurations and components, across different Wheel packages + - First-class cross-compilation support + - Reproducible Wheels and source distributions - No dependency on [setuptools](https://github.com/pypa/setuptools) - Compatible with [cibuildwheel](https://github.com/pypa/cibuildwheel) for building Wheels @@ -34,7 +34,7 @@ pip install py-build-cmake The documentation can be found on . -The format of the configuration file is explained in +The format of the configuration file is explained in [Config.md](https://tttapa.github.io/py-build-cmake/Config.html). Alternatively, use the [command-line interface](https://tttapa.github.io/py-build-cmake/CLI.html) @@ -43,11 +43,15 @@ to get the documentation for all supported options: py-build-cmake config format ``` +To get started quickly, have a look at the following section and the README in +[`examples/minimal`](https://github.com/tttapa/py-build-cmake/tree/main/examples/minimal), +which goes over the project structure and the configuration files you'll need. + ## Usage If you don't have one already, add a `pyproject.toml` configuration file to your -project's repository. Specify the metadata required by [PEP 621](https://www.python.org/dev/peps/pep-0621/), -and tell py-build-cmake how to build your project. For example: +project's repository. Specify the mandatory project metadata ([PyPA: Declaring project metadata](https://packaging.python.org/en/latest/specifications/declaring-project-metadata)), +and tell py-build-cmake how to build your CMake project. For example: ```toml [project] # Project metadata @@ -63,7 +67,7 @@ dependencies = ["numpy"] dynamic = ["version", "description"] [build-system] # How pip and other frontends should build this project -requires = ["py-build-cmake~=0.1.8"] +requires = ["py-build-cmake~=0.3.0"] build-backend = "py_build_cmake.build" [tool.py-build-cmake.module] # Where to find the Python module to package @@ -106,7 +110,12 @@ using a very simple Python module as an example. For a more advanced, real-world example, see [`examples/pybind11-project`](https://github.com/tttapa/py-build-cmake/tree/main/examples/pybind11-project) and [`examples/nanobind-project`](https://github.com/tttapa/py-build-cmake/tree/main/examples/nanobind-project). If you are interested in packaging C/C++/Fortran programs using py-build-cmake, -have a look at [`examples/minimal-program`](https://github.com/tttapa/py-build-cmake/tree/main/examples/minimal-program). +have a look at [`examples/minimal-program`](https://github.com/tttapa/py-build-cmake/tree/main/examples/minimal-program). +See the [`examples`](https://github.com/tttapa/py-build-cmake/tree/main/examples) folder for a full list of examples. + +A full example that uses the Conan package manager for C++ dependencies, and +that uses GitHub Actions to deploy the Wheel packages built by py-build-cmake +to PyPI can be found in [tttapa/py-build-cmake-example](https://github.com/tttapa/py-build-cmake-example). ## Projects using py-build-cmake @@ -116,9 +125,17 @@ py-build-cmake as their Python build backend: - [alpaqa](https://github.com/kul-optec/alpaqa/tree/develop) - [QPALM](https://github.com/kul-optec/QPALM) +## Alternatives and related tools + +- [scikit-build-core](https://github.com/scikit-build/scikit-build-core): alternative CMake build backend, successor of [scikit-build](https://github.com/scikit-build/scikit-build) +- [meson-python](https://github.com/mesonbuild/meson-python): Meson build backend +- [flit](https://github.com/pypa/flit): pure-Python packaging tool and build backend +- [hatchling](https://hatch.pypa.io/latest/config/build/#build-system): build backend of the [Hatch](https://hatch.pypa.io/latest/) project manager, supports build hooks +- [poetry-core](https://python-poetry.org/docs/pyproject/#poetry-and-pep-517): pure-Python build backend for the [Poetry](https://python-poetry.org/) package manager +- [crossenv](https://github.com/benfogle/crossenv): tool to trick `setuptools` into cross-compiling by monkey patching the `sysconfig` and `distutils` modules + ## Planned features - [x] ~~macOS support~~ - [x] ~~Entry point support~~ - - [ ] Namespace package support ([PEP 420](https://www.python.org/dev/peps/pep-0420/)) - - [ ] Doxygen and Sphinx support + - [x] ~~Namespace package support ([PEP 420](https://www.python.org/dev/peps/pep-0420/))~~ diff --git a/docs/Components.md b/docs/Components.md index a42c4f5..05817cf 100644 --- a/docs/Components.md +++ b/docs/Components.md @@ -7,17 +7,17 @@ A possible use case is distributing debug symbols: these files can be large, and See [examples/minimal-debug-component](https://github.com/tttapa/py-build-cmake/tree/main/examples/minimal-debug-component) for more information. +The most important option is `main_project`, which is a relative path that points to the directory containing the`pyproject.toml` of the main package (where all CMake options are defined). Next, the options in the `component` section define which CMake projects and components should be installed in this component package. + ## component Options for a separately packaged component. | Option | Description | Type | Default | |--------|-------------|------|---------| -| `main_project` | Directory containing the main pyproject.toml file.
Relative to project directory. | path | `'..'` | | `build_presets` | CMake presets to use for building. Passed as `--preset ` during the build phase, once for each preset. | list | `none` | -| `install_presets` | CMake presets to use for installing. Passed as `--preset ` during the installation phase, once for each preset. | list | `build_presets` | -| `build_args` | Extra arguments passed to the build step.
For example: `build_args = ["-j", "--target", "foo"]` | list | `none` | -| `build_tool_args` | Extra arguments passed to the build tool in the build step (e.g. to Make or Ninja).
For example: `build_tool_args = ["--verbose", "-d", "explain"]` | list | `none` | +| `build_args` | Extra arguments passed to the build step.
For example: `build_args = ["-j", "--target", "foo"]` | list+ | `none` | +| `build_tool_args` | Extra arguments passed to the build tool in the build step (e.g. to Make or Ninja).
For example: `build_tool_args = ["--verbose", "-d", "explain"]` | list+ | `none` | | `install_only` | Do not build the project, only install it.
For example: `install_only = true` | bool | `false` | -| `install_args` | Extra arguments passed to the install step.
For example: `install_args = ["--strip"]` | list | `none` | +| `install_args` | Extra arguments passed to the install step.
For example: `install_args = ["--strip"]` | list+ | `none` | | `install_components` | List of components to install, the install step is executed once for each component, with the option `--component `. | list | `required` | diff --git a/docs/Config.md b/docs/Config.md index cd9ea04..f8b62f9 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -11,13 +11,16 @@ Defines the import name of the module or package, and the directory where it can |--------|-------------|------|---------| | `name` | Import name in Python (can be different from the name on PyPI, which is defined in the [project] section). | string | `/pyproject.toml/project/name` | | `directory` | Directory containing the Python module/package.
Relative to project directory. | path | `'.'` | +| `namespace` | Set to true for PEP 420 namespace packages. | bool | `false` | +| `generated` | Do not try to locate the main module in the source directory, but assume that it is generated by CMake. Dynamic metadata cannot be used when set. | `'module'` \| `'package'` | `none` | ## editable Defines how to perform an editable install (PEP 660). See https://tttapa.github.io/py-build-cmake/Editable-install.html for more information. | Option | Description | Type | Default | |--------|-------------|------|---------| -| `mode` | Mechanism to use for editable installations. Either write a wrapper \_\_init\_\_.py file, install an import hook, or install symlinks to the original files. | `'wrapper'` \| `'hook'` \| `'symlink'` | `'wrapper'` | +| `mode` | Mechanism to use for editable installations. Either write a wrapper \_\_init\_\_.py file, install an import hook, or install symlinks to the original files. | `'wrapper'` \| `'hook'` \| `'symlink'` | `'symlink'` | +| `build_hook` | Automatically re-build any changed files and C extension modules when the package is first imported by Python. It is recommended to use a fast generator like Ninja. Currently, the only mode that supports build hooks is `symlink`. | bool | `false` | ## sdist Specifies the files that should be included in the source distribution for this package. @@ -32,25 +35,38 @@ Defines how to build the project to package. If omitted, py-build-cmake will pro | Option | Description | Type | Default | |--------|-------------|------|---------| -| `minimum_version` | Minimum required CMake version. If this version is not available in the system PATH, it will be installed automatically as a build dependency.
For example: `minimum_version = "3.18"` | string | `none` | +| `minimum_version` | Minimum required CMake version. Used for policies in the automatically generated CMake cache pre-load files. If this version is not available in the system PATH, it will be installed automatically as a build dependency (using Pip).
For example: `minimum_version = "3.18"` | string | `'3.15'` | | `build_type` | Build type passed to the configuration step, as `-DCMAKE_BUILD_TYPE=`.
For example: `build_type = "RelWithDebInfo"` | string | `none` | -| `config` | Configuration type passed to the build and install steps, as `--config `. You can specify either a single string, or a list of strings. If a multi-config generator is used, all configurations in this list will be included in the package.
For example: `config = ["Debug", "Release"]` | list | `build_type` | +| `config` | Configuration type passed to the build step, as `--config `. You can specify either a single string, or a list of strings. If a multi-config generator is used, all configurations in this list will be built.
For example: `config = ["Debug", "Release"]` | list | `build_type` | | `preset` | CMake preset to use for configuration. Passed as `--preset ` during the configuration phase. | string | `none` | -| `build_presets` | CMake presets to use for building. Passed as `--preset ` during the build phase, once for each preset. | list | `preset` | -| `install_presets` | CMake presets to use for installing. Passed as `--preset ` during the installation phase, once for each preset. | list | `build_presets` | +| `build_presets` | CMake presets to use for building. Passed as `--preset ` during the build phase, once for each preset. | list | `none` | | `generator` | CMake generator to use, passed to the configuration step, as `-G `. If Ninja is used, and if it is not available in the system PATH, it will be installed automatically as a build dependency.
For example: `generator = "Ninja Multi-Config"` | string | `none` | | `source_path` | Folder containing CMakeLists.txt.
Relative to project directory. | path | `'.'` | -| `build_path` | CMake build and cache folder.
Absolute or relative to project directory. | path | `'.py-build-cmake_cache'` | -| `options` | Extra options passed to the configuration step, as `-D