Skip to content
Open
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
16 changes: 15 additions & 1 deletion .github/workflows/cibuildwheel-impl/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ inputs:
build-tag:
description: 'The tag for this build'
required: true
artifact-name:
description: 'The name of the artifact to upload'
required: true

runs:
using: "composite"
Expand All @@ -13,8 +16,19 @@ runs:
env:
CIBW_BUILD: ${{ inputs.build-tag }}

- name: Setup Python for stripping pcms
uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Strip pre-built pcms
shell: bash
run: |
python -m pip install --quiet wheel
Comment thread
siliataider marked this conversation as resolved.
python .github/workflows/utilities/strip_wheel_pcms.py wheelhouse

- name: Upload wheel
uses: actions/upload-artifact@v6
with:
name: ${{ inputs.build-tag }}
name: ${{ inputs.artifact-name }}
path: wheelhouse/*
95 changes: 70 additions & 25 deletions .github/workflows/python_wheel_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,35 +50,80 @@
- "30"
required: true
default: "00"
pull_request:
types: [opened, synchronize, reopened]

jobs:
Build_Wheel:
runs-on: macos-26
runs-on: ${{ matrix.config.runner }}
strategy:
fail-fast: false
matrix:
python: [cp310, cp311, cp312, cp313, cp314]
config:
- runner: macos-15
platform: macosx_arm64
deployment_target: "15.0"
- runner: macos-26
platform: macosx_arm64
deployment_target: "26.0"
- runner: ubuntu-latest
platform: manylinux_x86_64
deployment_target: ""
name: ${{ matrix.python }}-${{ matrix.config.platform }}-${{ matrix.config.runner }}
steps:
- name: Checkout the official ROOT repo
uses: actions/checkout@v6
with:
path: root
# ref: v${{ inputs.major }}-${{ inputs.minor }}-${{ inputs.patch }}
- name: Make wheel
- uses: actions/checkout@v6
- uses: ./.github/workflows/cibuildwheel-impl
env:
MACOSX_DEPLOYMENT_TARGET: 26.5
run: |
mkdir wheel_creation && cd wheel_creation
cp -r ../root .
python3 -m venv root_build_env
source root_build_env/bin/activate
pip install --upgrade pip setuptools wheel build
cd root
pip install -r requirements.txt
python3 -m build --wheel
pip install delocate
mkdir fixed_wheels
delocate-wheel -v -w fixed_wheels/ dist/*.whl
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.config.deployment_target }}
with:
build-tag: ${{ matrix.python }}-${{ matrix.config.platform }}
artifact-name: ${{ matrix.python }}-${{ matrix.config.platform }}-${{ matrix.config.runner }}

- name: Upload_wheel
uses: actions/upload-artifact@v6
Test_Wheels:
needs: Build_Wheel
runs-on: ${{ matrix.test_config.runner }}
strategy:
fail-fast: false
matrix:
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
test_config:
# native tests: each runner tests its own wheel
- runner: macos-15
wheel_pattern: "macosx_15"
label: "native"
- runner: macos-26
wheel_pattern: "macosx_26"
label: "native"
- runner: ubuntu-latest
wheel_pattern: "manylinux"
label: "native"
# portability tests: oldest wheel on newer runners
- runner: macos-26
wheel_pattern: "macosx_15"
label: "port"
name: test-${{ matrix.python }}-${{ matrix.test_config.wheel_pattern }}-on-${{ matrix.test_config.runner }}-${{ matrix.test_config.label }}
steps:
- uses: actions/checkout@v6
- name: Download produced wheels
uses: actions/download-artifact@v4
with:
name: Wheel upload
path: /Users/runner/work/root/root/wheel_creation/root/fixed_wheels/*.whl
if-no-files-found: error
path: wheels
merge-multiple: true
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install produced wheel
run: |

Check failure on line 118 in .github/workflows/python_wheel_build.yml

View workflow job for this annotation

GitHub Actions / lint-action-files

shellcheck reported issue in this script: SC2012:info:3:9: Use find instead of ls to better handle non-alphanumeric filenames
ls -R wheels
PY_VER=$(python -c "import sys; print(f'cp{sys.version_info.major}{sys.version_info.minor}')")
WHEEL=$(ls wheels/*${PY_VER}*${{ matrix.test_config.wheel_pattern }}*.whl | head -n 1)
echo "Testing wheel: ${WHEEL} on ${{ matrix.test_config.runner }} (${{ matrix.test_config.label }})"
pip install "$WHEEL"
- name: Install tutorials dependencies
run: |
python -m pip install --no-cache-dir -r test/wheels/requirements-ci.txt
- name: Run tutorials
run: |
pytest -vv --verbosity="4" -rF test/wheels
60 changes: 60 additions & 0 deletions .github/workflows/utilities/strip_wheel_pcms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import argparse
import pathlib
import shutil
import subprocess
import sys
import tempfile

STRIP_PATTERNS = ("*.pcm", "modules.idx")


def strip_one_wheel(wheel_path: pathlib.Path) -> None:
with tempfile.TemporaryDirectory(prefix="strip-wheel-pcms-") as tmp:
tmp = pathlib.Path(tmp)
unpack_dir = tmp / "unpacked"
subprocess.run(
[sys.executable, "-m", "wheel", "unpack", str(wheel_path), "-d", str(unpack_dir)],
check=True,
)

# `wheel unpack` creates one <name>-<version> subdirectory
(extracted,) = list(unpack_dir.iterdir())

removed = []
for pattern in STRIP_PATTERNS:
for f in extracted.rglob(pattern):
f.unlink()
removed.append(str(f.relative_to(extracted)))

print(f"{wheel_path.name}: removed {len(removed)} file(s)")

repacked_dir = tmp / "repacked"
repacked_dir.mkdir()
subprocess.run(
[sys.executable, "-m", "wheel", "pack", str(extracted), "-d", str(repacked_dir)],
check=True,
)

(new_wheel,) = list(repacked_dir.glob("*.whl"))
wheel_path.unlink()
shutil.move(str(new_wheel), str(wheel_path))


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("directory", type=pathlib.Path, help="directory containing .whl files to strip")
args = parser.parse_args()

wheels = sorted(args.directory.glob("*.whl"))
if not wheels:
print(f"No .whl files found in {args.directory}", file=sys.stderr)
return 1

for wheel in wheels:
strip_one_wheel(wheel)

return 0


if __name__ == "__main__":
sys.exit(main())
6 changes: 6 additions & 0 deletions bindings/pyroot/pythonizations/python/ROOT/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@
except PackageNotFoundError:
pass

# Build every C++ module once per installation
# needed for the wheels, in other cases this is done in the CMake build step, so we skip it here
from . import _pcm_warmup

Check failure on line 218 in bindings/pyroot/pythonizations/python/ROOT/__init__.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E402)

bindings/pyroot/pythonizations/python/ROOT/__init__.py:218:1: E402 Module level import not at top of file help: Move module level imports to top of file

_pcm_warmup.warmup(_root_facade)


def _cleanup():
# Delete TBrowser instances while the GUI event loop is still alive,
Expand Down
86 changes: 86 additions & 0 deletions bindings/pyroot/pythonizations/python/ROOT/_pcm_warmup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Author: Silia Taider, CERN 08/2026

################################################################################
# Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. #
# All rights reserved. #
# #
# For the licensing terms see $ROOTSYS/LICENSE. #
# For the list of contributors see $ROOTSYS/README/CREDITS. #
################################################################################

"""
One-time warm-up that builds every ROOT C++ module ahead of use.
What we do: explicitly import every module declared in ROOT.modulemap,
once, right after the interpreter is up.
This only runs once per installation and is a no-op for builds that don't use C++
modules at all.
"""

import os
import re
import sys

_MODULE_LINE = re.compile(r'^module\s+"?([A-Za-z_][A-Za-z0-9_]*)"?\s*\{')


def _module_names(modulemap_path):
"""Extract the top-level module names declared in a Clang modulemap file"""
names = []
with open(modulemap_path) as f:
for line in f:
m = _MODULE_LINE.match(line)
if m:
names.append(m.group(1))
return names


def _sentinel_path(lib_dir):
"""Empty marker file to record that the module build run once"""
return os.path.join(lib_dir, ".pcm_warmup_complete")


def _mark_warmup_complete(lib_dir, sentinel):
try:
os.makedirs(lib_dir, exist_ok=True)
with open(sentinel, "w") as f:
f.write("1")
except OSError:
pass


def _print_progress(done, total, label):
width = 30
filled = width if total == 0 else int(width * done / total)
bar = "#" * filled + "-" * (width - filled)
sys.stderr.write(f"\r[{bar}] {done}/{total} building {label:<28}")
sys.stderr.flush()


def warmup(root_facade):
"""Build every C++ module once"""
this_dir = os.path.dirname(os.path.abspath(__file__))
modulemap_path = os.path.join(this_dir, "include", "ROOT.modulemap")
if not os.path.exists(modulemap_path):
# runtime_cxxmodules is off in this build
return

lib_dir = os.path.join(this_dir, "lib")
sentinel = _sentinel_path(lib_dir)
if os.path.exists(sentinel):
return

names = _module_names(modulemap_path)
if not names:
return

sys.stderr.write(f"ROOT: preparing this installation for your machine, this may take some time: {len(names)} modules...\n")
declare = root_facade.gInterpreter.Declare
for i, name in enumerate(names, 1):
_print_progress(i, len(names), name)
try:
declare(f"#pragma clang module import {name}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this out the pcms are being rebuild? If so, I am not sure this is the same as the pcms produced by rootcling. The pcms produced by rootcling contains not only the 'real' pcm but also have an additional payload which was previously the rootpcm files which contains shortcuts to the class information (in the form of TProtoClass stored in a ROOT file) which allows to be able to do I/O without actually loading any interpreter information (i.e. the real pcm content) which improve run-time and drastically reduce the memory needed.

except Exception:
pass
sys.stderr.write("\n")

_mark_warmup_complete(lib_dir, sentinel)
7 changes: 6 additions & 1 deletion core/base/src/TSystem.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -2276,6 +2276,11 @@ const char *TSystem::GetLibraries(const char *regexp, const char *options,

static TRegexp separator("[^ \\t\\s]+");
static TRegexp dynload("/lib-dynload/");
// Skip libffi, it is a private library used by the system. This is visible in the stub .tbd file:
// allowable-clients:
// clients: [ '!' ]
// See https://github.com/Homebrew/homebrew-core/issues/272324#issuecomment-5119880493 for more info
static TRegexp libffiMatch("/usr/lib/libffi");

Ssiz_t start, index, end;
start = index = end = 0;
Expand All @@ -2284,7 +2289,7 @@ const char *TSystem::GetLibraries(const char *regexp, const char *options,
index = libs2.Index(separator, &end, start);
if (index >= 0) {
TString s = libs2(index, end);
if (s.Index(dynload) == kNPOS) {
if (s.Index(dynload) == kNPOS && s.Index(libffiMatch) == kNPOS) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to (also) be introduced in #22963.
However, the location of this code is somewhat surprising and I wonder if it should be not be with the other vetoing in TCling::RegisterLoadedSharedLibrary. (Or at the very least to code comment why this one is here and the other there).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed this was merged in master but not in the branch root-project:test_macos_wheel_v2 so I just cherry picked it for the tests to work

The PRs I have open on this branch (#22886 and #23071) are just experimenting with the macos wheels, not going into production as they are of course

if (!maclibs.IsNull()) maclibs.Append(" ");
maclibs.Append(s);
}
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies = [

# Point backend to python packages and to explicitly use Ninja
[tool.scikit-build]
install.strip = false
wheel.packages = [
"bindings/pyroot/pythonizations/python/ROOT",
"bindings/pyroot/cppyy/cppyy/python/cppyy",
Expand Down Expand Up @@ -75,6 +76,7 @@ ssl="ON"
imt="ON"
roofit="ON"
mathmore="ON"
builtin_gif="ON"

# Expose the ROOT cli as an executable command via _rootcli wrapper
[project.scripts]
Expand Down
Loading