Skip to content
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
20 changes: 20 additions & 0 deletions .github/workflows/cibuildwheel-impl/action.yml
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/*
74 changes: 74 additions & 0 deletions .github/workflows/python_wheel_build.yml
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}/
25 changes: 25 additions & 0 deletions bindings/pyroot/pythonizations/python/ROOT/_rootcli.py
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())
8 changes: 6 additions & 2 deletions cmake/modules/SearchInstalledSoftware.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,11 @@ set(Python3_FIND_FRAMEWORK LAST)

# Even if we don't build PyROOT, one still need python executable to run some scripts
list(APPEND python_components Interpreter)
if(pyroot OR tmva-pymva)
if(pyroot AND NOT (tpython OR tmva-pymva))
# We have to only look for the Python development module in order to be able to build ROOT with a pip backend
# In particular, it is forbidden to link against libPython.so, see https://peps.python.org/pep-0513/#libpythonx-y-so-1
list(APPEND python_components Development.Module)
elseif(tpython OR tmva-pymva)
list(APPEND python_components Development)
endif()
if(tmva-pymva)
Expand Down Expand Up @@ -1759,7 +1763,7 @@ endif(tmva)
#---Check for PyROOT---------------------------------------------------------------------
if(pyroot)

if(Python3_Development_FOUND)
if(Python3_Development.Module_FOUND)
message(STATUS "PyROOT: development package found. Building for version ${Python3_VERSION}")
else()
if(fail-on-missing)
Expand Down
30 changes: 30 additions & 0 deletions pyproject.toml
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"
138 changes: 138 additions & 0 deletions setup.py
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 "
# 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"))
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},
)
Loading