mirror of https://github.com/google/benchmark.git
Modernize setup.py, extend Python bindings CI (#1535)
distutils is deprecated and will be removed in Python 3.12, so this commit modernizes the Python bindings `setup.py` file in order to future-proof the code. On top of this, type hints were added for all of the convenience functions to make static type checking adoption easier in the future, if desired. A context manager was added to temporarily write the Python include path to the Bazel WORKSPACE file - but unlike previously, the WORKSPACE file is reverted to its previous state after the build to not produce changes on every rebuild. Lastly, the Python bindings test matrix was extended to all major platforms to create a more complete picture of the current state of the bindings, especially with regards to upcoming wheel builds.
This commit is contained in:
parent
5e78bedfb0
commit
f59d021ebc
|
@ -8,17 +8,21 @@ on:
|
|||
|
||||
jobs:
|
||||
python_bindings:
|
||||
runs-on: ubuntu-latest
|
||||
name: Test GBM Python bindings on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install benchmark
|
||||
python-version: 3.11
|
||||
- name: Install GBM Python bindings on ${{ matrix.os}}
|
||||
run:
|
||||
python setup.py install
|
||||
- name: Run example bindings
|
||||
python -m pip install wheel .
|
||||
- name: Run bindings example on ${{ matrix.os }}
|
||||
run:
|
||||
python bindings/python/google_benchmark/example.py
|
||||
|
|
|
@ -18,5 +18,5 @@ pip3_install(
|
|||
new_local_repository(
|
||||
name = "python_headers",
|
||||
build_file = "@//bindings/python:python_headers.BUILD",
|
||||
path = "/usr/include/python3.6", # May be overwritten by setup.py.
|
||||
path = "<PYTHON_INCLUDE_PATH>", # May be overwritten by setup.py.
|
||||
)
|
||||
|
|
157
setup.py
157
setup.py
|
@ -1,44 +1,38 @@
|
|||
import contextlib
|
||||
import os
|
||||
import posixpath
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import sysconfig
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from distutils import sysconfig
|
||||
import setuptools
|
||||
from setuptools.command import build_ext
|
||||
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PYTHON_INCLUDE_PATH_PLACEHOLDER = "<PYTHON_INCLUDE_PATH>"
|
||||
|
||||
IS_WINDOWS = platform.system() == "Windows"
|
||||
IS_MAC = platform.system() == "Darwin"
|
||||
|
||||
|
||||
IS_WINDOWS = sys.platform.startswith("win")
|
||||
def _get_long_description(fp: str) -> str:
|
||||
with open(fp, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
with open("README.md", "r", encoding="utf-8") as fp:
|
||||
long_description = fp.read()
|
||||
def _get_version(fp: str) -> str:
|
||||
"""Parse a version string from a file."""
|
||||
with open(fp, "r") as f:
|
||||
for line in f:
|
||||
if "__version__" in line:
|
||||
delim = '"'
|
||||
return line.split(delim)[1]
|
||||
raise RuntimeError(f"could not find a version string in file {fp!r}.")
|
||||
|
||||
|
||||
def _get_version():
|
||||
"""Parse the version string from __init__.py."""
|
||||
with open(
|
||||
os.path.join(HERE, "bindings", "python", "google_benchmark", "__init__.py")
|
||||
) as init_file:
|
||||
try:
|
||||
version_line = next(
|
||||
line for line in init_file if line.startswith("__version__")
|
||||
)
|
||||
except StopIteration:
|
||||
raise ValueError("__version__ not defined in __init__.py")
|
||||
else:
|
||||
namespace = {}
|
||||
exec(version_line, namespace) # pylint: disable=exec-used
|
||||
return namespace["__version__"]
|
||||
|
||||
|
||||
def _parse_requirements(path):
|
||||
with open(os.path.join(HERE, path)) as requirements:
|
||||
def _parse_requirements(fp: str) -> List[str]:
|
||||
with open(fp) as requirements:
|
||||
return [
|
||||
line.rstrip()
|
||||
for line in requirements
|
||||
|
@ -46,15 +40,36 @@ def _parse_requirements(path):
|
|||
]
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def temp_fill_include_path(fp: str):
|
||||
"""Temporarily set the Python include path in a file."""
|
||||
with open(fp, "r+") as f:
|
||||
try:
|
||||
content = f.read()
|
||||
replaced = content.replace(
|
||||
PYTHON_INCLUDE_PATH_PLACEHOLDER,
|
||||
Path(sysconfig.get_paths()['include']).as_posix(),
|
||||
)
|
||||
f.seek(0)
|
||||
f.write(replaced)
|
||||
f.truncate()
|
||||
yield
|
||||
finally:
|
||||
# revert to the original content after exit
|
||||
f.seek(0)
|
||||
f.write(content)
|
||||
f.truncate()
|
||||
|
||||
|
||||
class BazelExtension(setuptools.Extension):
|
||||
"""A C/C++ extension that is defined as a Bazel BUILD target."""
|
||||
|
||||
def __init__(self, name, bazel_target):
|
||||
def __init__(self, name: str, bazel_target: str):
|
||||
super().__init__(name=name, sources=[])
|
||||
|
||||
self.bazel_target = bazel_target
|
||||
self.relpath, self.target_name = posixpath.relpath(bazel_target, "//").split(
|
||||
":"
|
||||
)
|
||||
setuptools.Extension.__init__(self, name, sources=[])
|
||||
stripped_target = bazel_target.split("//")[-1]
|
||||
self.relpath, self.target_name = stripped_target.split(":")
|
||||
|
||||
|
||||
class BuildBazelExtension(build_ext.build_ext):
|
||||
|
@ -65,67 +80,51 @@ class BuildBazelExtension(build_ext.build_ext):
|
|||
self.bazel_build(ext)
|
||||
build_ext.build_ext.run(self)
|
||||
|
||||
def bazel_build(self, ext):
|
||||
def bazel_build(self, ext: BazelExtension):
|
||||
"""Runs the bazel build to create the package."""
|
||||
with open("WORKSPACE", "r") as workspace:
|
||||
workspace_contents = workspace.read()
|
||||
with temp_fill_include_path("WORKSPACE"):
|
||||
temp_path = Path(self.build_temp)
|
||||
|
||||
with open("WORKSPACE", "w") as workspace:
|
||||
workspace.write(
|
||||
re.sub(
|
||||
r'(?<=path = ").*(?=", # May be overwritten by setup\.py\.)',
|
||||
sysconfig.get_python_inc().replace(os.path.sep, posixpath.sep),
|
||||
workspace_contents,
|
||||
)
|
||||
)
|
||||
bazel_argv = [
|
||||
"bazel",
|
||||
"build",
|
||||
str(ext.bazel_target),
|
||||
f"--symlink_prefix={temp_path / 'bazel-'}",
|
||||
f"--compilation_mode={'dbg' if self.debug else 'opt'}",
|
||||
]
|
||||
|
||||
if not os.path.exists(self.build_temp):
|
||||
os.makedirs(self.build_temp)
|
||||
if IS_WINDOWS:
|
||||
# Link with python*.lib.
|
||||
for library_dir in self.library_dirs:
|
||||
bazel_argv.append("--linkopt=/LIBPATH:" + library_dir)
|
||||
elif IS_MAC and platform.machine() == "x86_64":
|
||||
bazel_argv.append("--macos_minimum_os=10.9")
|
||||
|
||||
bazel_argv = [
|
||||
"bazel",
|
||||
"build",
|
||||
ext.bazel_target,
|
||||
"--symlink_prefix=" + os.path.join(self.build_temp, "bazel-"),
|
||||
"--compilation_mode=" + ("dbg" if self.debug else "opt"),
|
||||
]
|
||||
# ARCHFLAGS is always set by cibuildwheel before macOS wheel builds.
|
||||
archflags = os.getenv("ARCHFLAGS", "")
|
||||
if "arm64" in archflags:
|
||||
bazel_argv.append("--cpu=darwin_arm64")
|
||||
bazel_argv.append("--macos_cpus=arm64")
|
||||
|
||||
if IS_WINDOWS:
|
||||
# Link with python*.lib.
|
||||
for library_dir in self.library_dirs:
|
||||
bazel_argv.append("--linkopt=/LIBPATH:" + library_dir)
|
||||
elif sys.platform == "darwin" and platform.machine() == "x86_64":
|
||||
bazel_argv.append("--macos_minimum_os=10.9")
|
||||
self.spawn(bazel_argv)
|
||||
|
||||
# ARCHFLAGS is always set by cibuildwheel before macOS wheel builds.
|
||||
archflags = os.getenv("ARCHFLAGS", "")
|
||||
if "arm64" in archflags:
|
||||
bazel_argv.append("--cpu=darwin_arm64")
|
||||
bazel_argv.append("--macos_cpus=arm64")
|
||||
shared_lib_suffix = '.dll' if IS_WINDOWS else '.so'
|
||||
ext_name = ext.target_name + shared_lib_suffix
|
||||
ext_bazel_bin_path = temp_path / 'bazel-bin' / ext.relpath / ext_name
|
||||
|
||||
self.spawn(bazel_argv)
|
||||
ext_dest_path = Path(self.get_ext_fullpath(ext.name))
|
||||
shutil.copyfile(ext_bazel_bin_path, ext_dest_path)
|
||||
|
||||
shared_lib_suffix = '.dll' if IS_WINDOWS else '.so'
|
||||
ext_bazel_bin_path = os.path.join(
|
||||
self.build_temp, 'bazel-bin',
|
||||
ext.relpath, ext.target_name + shared_lib_suffix)
|
||||
|
||||
ext_dest_path = self.get_ext_fullpath(ext.name)
|
||||
ext_dest_dir = os.path.dirname(ext_dest_path)
|
||||
if not os.path.exists(ext_dest_dir):
|
||||
os.makedirs(ext_dest_dir)
|
||||
shutil.copyfile(ext_bazel_bin_path, ext_dest_path)
|
||||
|
||||
# explicitly call `bazel shutdown` for graceful exit
|
||||
self.spawn(["bazel", "shutdown"])
|
||||
# explicitly call `bazel shutdown` for graceful exit
|
||||
self.spawn(["bazel", "shutdown"])
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
name="google_benchmark",
|
||||
version=_get_version(),
|
||||
version=_get_version("bindings/python/google_benchmark/__init__.py"),
|
||||
url="https://github.com/google/benchmark",
|
||||
description="A library to benchmark code snippets.",
|
||||
long_description=long_description,
|
||||
long_description=_get_long_description("README.md"),
|
||||
long_description_content_type="text/markdown",
|
||||
author="Google",
|
||||
author_email="benchmark-py@google.com",
|
||||
|
|
Loading…
Reference in New Issue