-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add workflow to build Python wheels #19456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| name: 'cibuildwheel-impl' | ||
| description: 'Run cibuildwheel to produce a wheel depending on input tag' | ||
| inputs: | ||
| build-tag: | ||
| description: 'The tag for this build' | ||
| required: true | ||
|
|
||
| runs: | ||
| using: "composite" | ||
| steps: | ||
| - name: Build wheel | ||
| uses: pypa/cibuildwheel@v3.0.1 | ||
| env: | ||
| CIBW_BUILD: ${{ inputs.build-tag }} | ||
|
|
||
| - name: Upload wheel | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: ${{ inputs.build-tag }} | ||
| path: wheelhouse/* |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| name: 'ROOT Python wheels' | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| branch: | ||
| description: 'The branch for which the Python wheel has to be generated' | ||
| type: string | ||
| required: true | ||
| default: "master" | ||
| push: | ||
| branches: experimental-pip-install-root | ||
| schedule: | ||
| - cron: '01 1 * * *' | ||
|
|
||
| jobs: | ||
| build-wheels: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| matrix: | ||
| target: [cp38-manylinux_x86_64, cp39-manylinux_x86_64, cp310-manylinux_x86_64, cp311-manylinux_x86_64, cp312-manylinux_x86_64, cp313-manylinux_x86_64] | ||
| name: ${{ matrix.target }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: ./.github/workflows/cibuildwheel-impl | ||
| with: | ||
| build-tag: ${{ matrix.target }} | ||
|
|
||
| create-and-upload-wheel-registry: | ||
| needs: build-wheels | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
|
|
||
| - name: Download produced wheels | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| merge-multiple: true | ||
|
|
||
| - name: Install required system packages | ||
| run: sudo apt-get install -y krb5-user xrootd-client | ||
|
|
||
| - name: Setup Python | ||
| uses: actions/setup-python@v5 | ||
|
|
||
| - name: Create PEP503-compliant wheel registry | ||
| run: | | ||
| mkdir -p wheelhouse | ||
| mv *.whl wheelhouse | ||
| python -m pip install --upgrade pip | ||
| python -m pip install --user simple503 | ||
| simple503 wheelhouse | ||
|
|
||
| - name: Compute number of files to upload | ||
| id: nfiles | ||
| run: echo "NFILES=$(find wheelhouse -maxdepth 1 | wc -l)" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Upload wheels to EOS | ||
| env: | ||
| RWEBEOS_KT: ${{ secrets.RWEBEOS_KT }} | ||
| KT_FILE_NAME: /tmp/decoded.keytab | ||
| EOS_PATH: /eos/project/r/root-eos/www/experimental-python-wheels | ||
| EOS_ENDPOINT: root://eosproject-r.cern.ch | ||
| KRB5CCNAME: /tmp/krb5cc | ||
| NFILES: ${{ steps.nfiles.outputs.NFILES }} | ||
| working-directory: ${{ env.WORKDIR }} | ||
| run: | | ||
| echo +++ Content | ||
| ls | ||
| echo +++ Retrieving the secret | ||
| echo ${RWEBEOS_KT} | base64 -d > ${KT_FILE_NAME} | ||
| echo +++ Creating the token | ||
| kinit -p ${{ secrets.KRB5USER }}@${{ secrets.KRB5REALM }} -kt ${KT_FILE_NAME} | ||
| echo +++ Running the copy | ||
| xrdcp --parallel ${NFILES} -rf wheelhouse/* ${EOS_ENDPOINT}/${EOS_PATH}/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import os | ||
| import subprocess | ||
| import sys | ||
|
|
||
| ROOT_HOME = os.path.dirname(__file__) | ||
|
|
||
|
|
||
| def main() -> None: | ||
| # Find the ROOT executable from the current installation directory | ||
| bindir = os.path.join(ROOT_HOME, "bin") | ||
| rootexe = os.path.join(bindir, "root.exe") | ||
| if not os.path.exists(rootexe): | ||
| raise FileNotFoundError( | ||
| f"Could not find 'root.exe' executable in directory '{bindir}'. " | ||
| "Something is wrong in the ROOT installation." | ||
| ) | ||
| # Make sure command line arguments are preserved | ||
| args = [rootexe] + sys.argv[1:] | ||
| # Run the actual ROOT executable and return the exit code to the main Python process | ||
| out = subprocess.run(args) | ||
| return out.returncode | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| [build-system] | ||
| requires = ["cmake", "setuptools<72", "wheel", "numpy"] | ||
|
|
||
| [project] | ||
| name = "ROOT" | ||
| version = "0.1a8" | ||
| requires-python = ">=3.8" | ||
| maintainers = [ | ||
| {name = "Vincenzo Eduardo Padulano", email = "vincenzo.eduardo.padulano@cern.ch"} | ||
| ] | ||
| description = "An open-source data analysis framework used by high energy physics and others." | ||
| readme = "README.md" | ||
| license = {file = "LICENSE"} | ||
| dependencies = [ | ||
| "numpy", | ||
| ] | ||
|
|
||
| # Demonstrate how to expose ROOT CLI tools the Python way. For now only include | ||
| # `root`, other tools can be added later using the same approach. | ||
| [project.scripts] | ||
| root = "ROOT._rootcli:main" | ||
|
|
||
| [tool.cibuildwheel] | ||
| # Increase pip debugging output | ||
| build-verbosity = 1 | ||
| manylinux-x86_64-image = "manylinux_2_28" | ||
|
|
||
| # Install system libraries | ||
| [tool.cibuildwheel.linux] | ||
| before-all = "dnf install -y epel-release && /usr/bin/crb enable && dnf install -y openssl-devel libX11-devel libXpm-devel libXft-devel libXext-devel libuuid-devel libjpeg-devel giflib-devel" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| """ | ||
| setuptools-based build of ROOT. | ||
|
|
||
| This script uses setuptools API to steer a custom CMake build of ROOT. All build | ||
| configuration options are specified in the class responsible for building. A | ||
| custom extension module is injected in the setuptools setup to properly generate | ||
| the wheel with CPython extension metadata. Note that ROOT is first installed via | ||
| CMake into a temporary directory, then the ROOT installation artifacts are moved | ||
| to the final Python environment installation path, which often starts at | ||
| ${ENV_PREFIX}/lib/pythonXX.YY/site-packages, before being packaged as a wheel. | ||
| """ | ||
|
|
||
| import os | ||
| import pathlib | ||
| import shlex | ||
| import subprocess | ||
| import tempfile | ||
|
|
||
| from setuptools import Extension, find_packages, setup | ||
| from setuptools.command.build import build as _build | ||
| from setuptools.command.install import install as _install | ||
|
|
||
| # Get the long description from the README file | ||
| SOURCE_DIR = pathlib.Path(__file__).parent.resolve() | ||
| LONG_DESCRIPTION = (SOURCE_DIR / "README.md").read_text(encoding="utf-8") | ||
|
|
||
| BUILD_DIR = tempfile.mkdtemp() | ||
| INSTALL_DIR = tempfile.mkdtemp() | ||
|
|
||
| # Name given to an internal directory within the build directory | ||
| # used to mimick the structure of the target installation directory | ||
| # in the user Python environment, usually named "site-packages" | ||
| ROOT_BUILD_INTERNAL_DIRNAME = "mock_site_packages" | ||
|
|
||
|
|
||
| class ROOTBuild(_build): | ||
| def run(self): | ||
| _build.run(self) | ||
|
|
||
| # Configure ROOT build | ||
| configure_command = shlex.split( | ||
| "cmake " | ||
| # gminimal=ON enables only a minimal set of components (cling+core+I/O+graphics) | ||
| "-Dgminimal=ON -Dasimage=ON -Dopengl=OFF " | ||
| "-Druntime_cxxmodules=ON -Drpath=ON -Dfail-on-missing=ON " # Generic build configuration | ||
| # Explicitly turned off components, even though they are already off because of gminimal, we want to keep | ||
| # them listed here for documentation purposes: | ||
| # - tmva-pymva, tpython: these components link against libPython, forbidden for manylinux compatibility, | ||
| # see https://peps.python.org/pep-0513/#libpythonx-y-so-1 | ||
| # - thisroot_scripts: the thisroot.* scripts are broken if CMAKE_INSTALL_PYTHONDIR!=CMAKE_INSTALL_LIBDIR | ||
| "-Dtmva-pymva=OFF -Dtpython=OFF -Dthisroot_scripts=OFF " | ||
| "-Dbuiltin_nlohmannjson=ON -Dbuiltin_tbb=ON -Dbuiltin_xrootd=ON " # builtins | ||
| "-Dbuiltin_lz4=ON -Dbuiltin_lzma=ON -Dbuiltin_zstd=ON -Dbuiltin_xxhash=ON " # builtins | ||
| "-Dpyroot=ON -Ddataframe=ON -Dxrootd=ON -Dssl=ON -Dimt=ON " | ||
| "-Droofit=ON " | ||
vepadulano marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # Next 4 paths represent the structure of the target binaries/headers/libs | ||
| # as the target installation directory of the Python environment would expect | ||
| f"-DCMAKE_INSTALL_BINDIR={ROOT_BUILD_INTERNAL_DIRNAME}/ROOT/bin " | ||
| f"-DCMAKE_INSTALL_INCLUDEDIR={ROOT_BUILD_INTERNAL_DIRNAME}/ROOT/include " | ||
| f"-DCMAKE_INSTALL_LIBDIR={ROOT_BUILD_INTERNAL_DIRNAME}/ROOT/lib " | ||
| f"-DCMAKE_INSTALL_PYTHONDIR={ROOT_BUILD_INTERNAL_DIRNAME} " | ||
| f"-DCMAKE_INSTALL_PREFIX={INSTALL_DIR} -B {BUILD_DIR} -S {SOURCE_DIR}" | ||
| ) | ||
| subprocess.run(configure_command, check=True) | ||
|
|
||
| # Run build with CMake | ||
| build_command = f"cmake --build {BUILD_DIR} -j{os.cpu_count()}" | ||
| subprocess.run(shlex.split(build_command), check=True) | ||
|
|
||
|
|
||
| class ROOTInstall(_install): | ||
| def _get_install_path(self): | ||
| if hasattr(self, "bdist_dir") and self.bdist_dir: | ||
| install_path = self.bdist_dir | ||
| else: | ||
| install_path = self.install_lib | ||
|
|
||
| return install_path | ||
|
|
||
| def run(self): | ||
| _install.run(self) | ||
|
|
||
| install_cmd = f"cmake --build {BUILD_DIR} --target install" | ||
| subprocess.run(shlex.split(install_cmd), check=True) | ||
|
|
||
| install_path = self._get_install_path() | ||
|
|
||
| # Copy ROOT installation tree to the ROOT package directory in the pip installation path | ||
| self.copy_tree(os.path.join(INSTALL_DIR, ROOT_BUILD_INTERNAL_DIRNAME), install_path) | ||
|
|
||
| root_package_dir = os.path.join(install_path, "ROOT") | ||
|
|
||
| # After the copy of the "mock" package structure from the ROOT installations, these are the | ||
| # leftover directories that still need to be copied | ||
| self.copy_tree(os.path.join(INSTALL_DIR, "cmake"), os.path.join(root_package_dir, "cmake")) | ||
| self.copy_tree(os.path.join(INSTALL_DIR, "etc"), os.path.join(root_package_dir, "etc")) | ||
| self.copy_tree(os.path.join(INSTALL_DIR, "fonts"), os.path.join(root_package_dir, "fonts")) | ||
| self.copy_tree(os.path.join(INSTALL_DIR, "icons"), os.path.join(root_package_dir, "icons")) | ||
| self.copy_tree(os.path.join(INSTALL_DIR, "macros"), os.path.join(root_package_dir, "macros")) | ||
| self.copy_tree(os.path.join(INSTALL_DIR, "man"), os.path.join(root_package_dir, "man")) | ||
| self.copy_tree(os.path.join(INSTALL_DIR, "README"), os.path.join(root_package_dir, "README")) | ||
| self.copy_tree(os.path.join(INSTALL_DIR, "tutorials"), os.path.join(root_package_dir, "tutorials")) | ||
guitargeek marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self.copy_file(os.path.join(INSTALL_DIR, "LICENSE"), os.path.join(root_package_dir, "LICENSE")) | ||
|
|
||
| def get_outputs(self): | ||
| outputs = _install.get_outputs(self) | ||
| return outputs | ||
|
|
||
|
|
||
| class DummyExtension(Extension): | ||
| """ | ||
| Dummy CPython extension for setuptools setup. | ||
|
|
||
| In order to generate the wheel with CPython extension metadata (i.e. | ||
| producing one wheel per supported Python version), setuptools requires that | ||
| at least one CPython extension is declared in the `ext_modules` kwarg passed | ||
| to the `setup` function. Usually, declaring a CPython extension triggers | ||
| compilation of the corresponding sources, but in this case we already do | ||
| that in the CMake build step. This class defines a dummy extension that | ||
| can be declared to setuptools while avoiding any further compilation step. | ||
| """ | ||
|
|
||
| def __init__(_): | ||
| super().__init__(name="Dummy", sources=[]) | ||
|
|
||
|
|
||
| pkgs = find_packages("bindings/pyroot/pythonizations/python") + find_packages( | ||
| "bindings/pyroot/cppyy/cppyy/python", include=["cppyy"] | ||
| ) | ||
|
|
||
| s = setup( | ||
| long_description=LONG_DESCRIPTION, | ||
| package_dir={"": "bindings/pyroot/pythonizations/python", "cppyy": "bindings/pyroot/cppyy/cppyy/python"}, | ||
| packages=pkgs, | ||
| # Crucial to signal this is not a pure Python package | ||
| ext_modules=[DummyExtension()], | ||
| cmdclass={"build": ROOTBuild, "install": ROOTInstall}, | ||
| ) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.