From f98575b114e6e64f52619996d55e089e3f049ba9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 28 Mar 2026 05:52:58 +0000 Subject: [PATCH] update pyzbar shim --- pyzbar/pyproject.toml | 20 +++ pyzbar/pyzbar/__init__.py | 22 +++ pyzbar/pyzbar/locations.py | 70 ++++++++ pyzbar/pyzbar/pyzbar.py | 236 +++++++++++++++++++++++++++ pyzbar/pyzbar/pyzbar_error.py | 5 + pyzbar/pyzbar/wrapper.py | 295 ++++++++++++++++++++++++++++++++++ pyzbar/pyzbar/zbar_library.py | 22 +++ pyzbar/setup.py | 103 ++++++++++++ 8 files changed, 773 insertions(+) create mode 100644 pyzbar/pyproject.toml create mode 100644 pyzbar/pyzbar/__init__.py create mode 100644 pyzbar/pyzbar/locations.py create mode 100644 pyzbar/pyzbar/pyzbar.py create mode 100644 pyzbar/pyzbar/pyzbar_error.py create mode 100644 pyzbar/pyzbar/wrapper.py create mode 100644 pyzbar/pyzbar/zbar_library.py create mode 100644 pyzbar/setup.py diff --git a/pyzbar/pyproject.toml b/pyzbar/pyproject.toml new file mode 100644 index 0000000..f30c179 --- /dev/null +++ b/pyzbar/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["setuptools>=64", "wheel", 'tomli; python_version < "3.11"'] +build-backend = "setuptools.build_meta" + +[project] +name = "pyzbar" +version = "0.1.9" +description = "pyzbar barcode reader with bundled zbar library (pre-built)" +requires-python = ">=3.8" + +[tool.setuptools.packages.find] +include = ["pyzbar*"] + +[tool.setuptools.package-data] +pyzbar = ["install/**/*", "*.so"] + +[tool.shim] +repo_url = "https://github.com/greatgitsby/op-dependencies" +tag = "pyzbar/v0.1.9" +datadir = "install" diff --git a/pyzbar/pyzbar/__init__.py b/pyzbar/pyzbar/__init__.py new file mode 100644 index 0000000..5bd4c12 --- /dev/null +++ b/pyzbar/pyzbar/__init__.py @@ -0,0 +1,22 @@ +"""Read one-dimensional barcodes and QR codes from Python 2 and 3.""" +import os + +__version__ = '0.1.9' + +DIR = os.path.join(os.path.dirname(__file__), "install") +LIB_DIR = os.path.join(DIR, "lib") + + +def smoketest(): + import platform + + if platform.system() == "Darwin": + lib_name = "libzbar.dylib" + else: + lib_name = "libzbar.so" + lib_path = os.path.join(LIB_DIR, lib_name) + assert os.path.isfile(lib_path), f"{lib_name} not found at {lib_path}" + + from .pyzbar import decode + result = decode((b'\x00', 1, 1)) + assert isinstance(result, list), "decode() did not return a list" diff --git a/pyzbar/pyzbar/locations.py b/pyzbar/pyzbar/locations.py new file mode 100644 index 0000000..6ee14d8 --- /dev/null +++ b/pyzbar/pyzbar/locations.py @@ -0,0 +1,70 @@ +from collections import namedtuple +from itertools import chain +from operator import itemgetter + + +__all__ = ['bounding_box', 'convex_hull', 'Point', 'Rect'] + + +Point = namedtuple('Point', ['x', 'y']) +Rect = namedtuple('Rect', ['left', 'top', 'width', 'height']) + + +def bounding_box(locations): + """Computes the bounding box of an iterable of (x, y) coordinates. + + Args: + locations: iterable of (x, y) tuples. + + Returns: + `Rect`: Coordinates of the bounding box. + """ + x_values = list(map(itemgetter(0), locations)) + x_min, x_max = min(x_values), max(x_values) + y_values = list(map(itemgetter(1), locations)) + y_min, y_max = min(y_values), max(y_values) + return Rect(x_min, y_min, x_max - x_min, y_max - y_min) + + +def convex_hull(points): + """Computes the convex hull of an iterable of (x, y) coordinates. + + Args: + points: iterable of (x, y) tuples. + + Returns: + `list`: instances of `Point` - vertices of the convex hull in + counter-clockwise order, starting from the vertex with the + lexicographically smallest coordinates. + + Andrew's monotone chain algorithm. O(n log n) complexity. + https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain + """ + + def is_not_clockwise(p0, p1, p2): + return 0 <= ( + (p1[0] - p0[0]) * (p2[1] - p0[1]) - + (p1[1] - p0[1]) * (p2[0] - p0[0]) + ) + + def go(points_): + res = [] + for p in points_: + while 1 < len(res) and is_not_clockwise(res[-2], res[-1], p): + res.pop() + res.append(p) + + # The last point in each list is the first point in the other list + res.pop() + + return res + + # Discard duplicates and sort by x then y + points = sorted(set(points)) + + # Algorithm needs at least two points + hull = ( + points if len(points) < 2 else chain(go(points), go(reversed(points))) + ) + + return list(map(Point._make, hull)) diff --git a/pyzbar/pyzbar/pyzbar.py b/pyzbar/pyzbar/pyzbar.py new file mode 100644 index 0000000..5b09ec3 --- /dev/null +++ b/pyzbar/pyzbar/pyzbar.py @@ -0,0 +1,236 @@ +from collections import namedtuple +from contextlib import contextmanager +from ctypes import cast, c_void_p, string_at + +from .locations import bounding_box, convex_hull, Point, Rect +from .pyzbar_error import PyZbarError +from .wrapper import ( + zbar_image_scanner_set_config, + zbar_image_scanner_create, zbar_image_scanner_destroy, + zbar_image_create, zbar_image_destroy, zbar_image_set_format, + zbar_image_set_size, zbar_image_set_data, zbar_scan_image, + zbar_image_first_symbol, zbar_symbol_get_data_length, + zbar_symbol_get_data, zbar_symbol_get_orientation, + zbar_symbol_get_loc_size, zbar_symbol_get_loc_x, zbar_symbol_get_loc_y, + zbar_symbol_get_quality, zbar_symbol_next, ZBarConfig, ZBarOrientation, + ZBarSymbol, EXTERNAL_DEPENDENCIES, +) + +__all__ = [ + 'decode', 'Point', 'Rect', 'Decoded', 'ZBarSymbol', 'EXTERNAL_DEPENDENCIES', 'ORIENTATION_AVAILABLE' +] + + +ORIENTATION_AVAILABLE = zbar_symbol_get_orientation is not None + +Decoded = namedtuple('Decoded', 'data type rect polygon quality orientation') + +# ZBar's magic 'fourcc' numbers that represent image formats +_FOURCC = { + 'L800': 808466521, + 'GRAY': 1497715271 +} + +_RANGEFN = getattr(globals(), 'xrange', range) + + +@contextmanager +def _image(): + """A context manager for `zbar_image`, created and destoyed by + `zbar_image_create` and `zbar_image_destroy`. + + Yields: + POINTER(zbar_image): The created image + + Raises: + PyZbarError: If the image could not be created. + """ + image = zbar_image_create() + if not image: + raise PyZbarError('Could not create zbar image') + else: + try: + yield image + finally: + zbar_image_destroy(image) + + +@contextmanager +def _image_scanner(): + """A context manager for `zbar_image_scanner`, created and destroyed by + `zbar_image_scanner_create` and `zbar_image_scanner_destroy`. + + Yields: + POINTER(zbar_image_scanner): The created scanner + + Raises: + PyZbarError: If the decoder could not be created. + """ + scanner = zbar_image_scanner_create() + if not scanner: + raise PyZbarError('Could not create image scanner') + else: + try: + yield scanner + finally: + zbar_image_scanner_destroy(scanner) + + +def _symbols_for_image(image): + """Generator of symbols. + + Args: + image: `zbar_image` + + Yields: + POINTER(zbar_symbol): Symbol + """ + symbol = zbar_image_first_symbol(image) + while symbol: + yield symbol + symbol = zbar_symbol_next(symbol) + + +def _decode_symbols(symbols): + """Generator of decoded symbol information. + + Args: + symbols: iterable of instances of `POINTER(zbar_symbol)` + + Yields: + Decoded: decoded symbol + """ + for symbol in symbols: + data = string_at( + zbar_symbol_get_data(symbol), + zbar_symbol_get_data_length(symbol) + ) + # The 'type' int should be a value in the ZBarSymbol enumeration + try: + symbol_type = ZBarSymbol(symbol.contents.type) + except ValueError: + # This release of zbar supports a type that pyzbar does not know about + symbol_type = "Unrecognised type [{0}]".format(symbol.contents.type) + else: + symbol_type = symbol_type.name + + quality = zbar_symbol_get_quality(symbol) + polygon = convex_hull( + ( + zbar_symbol_get_loc_x(symbol, index), + zbar_symbol_get_loc_y(symbol, index) + ) + for index in _RANGEFN(zbar_symbol_get_loc_size(symbol)) + ) + + if zbar_symbol_get_orientation: + orientation = ZBarOrientation(zbar_symbol_get_orientation(symbol)).name + else: + orientation = None + + yield Decoded( + data=data, + type=symbol_type, + rect=bounding_box(polygon), + polygon=polygon, + orientation=orientation, + quality=quality, + ) + + +def _pixel_data(image): + """Returns (pixels, width, height) + + Returns: + :obj: `tuple` (pixels, width, height) + """ + # Test for PIL.Image, numpy.ndarray, and imageio.core.util without + # requiring that cv2, PIL, or imageio are installed. + + image_type = str(type(image)) + if 'PIL.' in image_type: + if 'L' != image.mode: + image = image.convert('L') + pixels = image.tobytes() + width, height = image.size + elif 'numpy.ndarray' in image_type or 'imageio.core.util' in image_type: + # Different versions of imageio use a subclass of numpy.ndarray + # called either imageio.core.util.Image or imageio.core.util.Array. + if 3 == len(image.shape): + # Take just the first channel + image = image[:, :, 0] + if 'uint8' != str(image.dtype): + image = image.astype('uint8') + try: + pixels = image.tobytes() + except AttributeError: + # `numpy.ndarray.tobytes()` introduced in `numpy` 1.9.0 - use the + # older `tostring` method. + pixels = image.tostring() + height, width = image.shape[:2] + else: + # image should be a tuple (pixels, width, height) + pixels, width, height = image + + # Check dimensions + if 0 != len(pixels) % (width * height): + raise PyZbarError( + ( + 'Inconsistent dimensions: image data of {0} bytes is not ' + 'divisible by (width x height = {1})' + ).format(len(pixels), (width * height)) + ) + + # Compute bits-per-pixel + bpp = 8 * len(pixels) // (width * height) + if 8 != bpp: + raise PyZbarError( + 'Unsupported bits-per-pixel [{0}]. Only [8] is supported.'.format( + bpp + ) + ) + + return pixels, width, height + + +def decode(image, symbols=None): + """Decodes datamatrix barcodes in `image`. + + Args: + image: `numpy.ndarray`, `PIL.Image` or tuple (pixels, width, height) + symbols: iter(ZBarSymbol) the symbol types to decode; if `None`, uses + `zbar`'s default behaviour, which is to decode all symbol types. + + Returns: + :obj:`list` of :obj:`Decoded`: The values decoded from barcodes. + """ + pixels, width, height = _pixel_data(image) + + results = [] + with _image_scanner() as scanner: + if symbols: + # Disable all but the symbols of interest + disable = set(ZBarSymbol).difference(symbols) + for symbol in disable: + zbar_image_scanner_set_config( + scanner, symbol, ZBarConfig.CFG_ENABLE, 0 + ) + # I think it likely that zbar will detect all symbol types by + # default, in which case enabling the types of interest is + # redundant but it seems sensible to be over-cautious and enable + # them. + for symbol in symbols: + zbar_image_scanner_set_config( + scanner, symbol, ZBarConfig.CFG_ENABLE, 1 + ) + with _image() as img: + zbar_image_set_format(img, _FOURCC['L800']) + zbar_image_set_size(img, width, height) + zbar_image_set_data(img, cast(pixels, c_void_p), len(pixels), None) + decoded = zbar_scan_image(scanner, img) + if decoded < 0: + raise PyZbarError('Unsupported image format') + else: + results.extend(_decode_symbols(_symbols_for_image(img))) + + return results diff --git a/pyzbar/pyzbar/pyzbar_error.py b/pyzbar/pyzbar/pyzbar_error.py new file mode 100644 index 0000000..3abdf02 --- /dev/null +++ b/pyzbar/pyzbar/pyzbar_error.py @@ -0,0 +1,5 @@ +__all__ = ["PyZbarError"] + + +class PyZbarError(Exception): + pass diff --git a/pyzbar/pyzbar/wrapper.py b/pyzbar/pyzbar/wrapper.py new file mode 100644 index 0000000..b9f333a --- /dev/null +++ b/pyzbar/pyzbar/wrapper.py @@ -0,0 +1,295 @@ +"""Low-level wrapper around zbar's interface +""" +from ctypes import ( + c_ubyte, c_char_p, c_int, c_uint, c_ulong, c_void_p, Structure, + CFUNCTYPE, POINTER +) +from enum import IntEnum, unique + +from . import zbar_library + + +__all__ = [ + 'EXTERNAL_DEPENDENCIES', 'LIBZBAR', 'ZBarConfig', 'ZBarSymbol', 'ZBarOrientation', + 'zbar_image_create', 'zbar_image_destroy', 'zbar_image_first_symbol', + 'zbar_image_scanner_create', 'zbar_image_scanner_destroy', + 'zbar_image_scanner_set_config', 'zbar_image_set_data', + 'zbar_image_set_format', 'zbar_image_set_size', 'zbar_scan_image', + 'zbar_symbol_get_data_length', 'zbar_symbol_get_data', + 'zbar_symbol_get_loc_size', 'zbar_symbol_get_loc_x', + 'zbar_symbol_get_loc_y', 'zbar_symbol_next', + 'zbar_symbol_get_orientation', 'zbar_symbol_get_quality', +] + +# Globals populated in load_libzbar +LIBZBAR = None +"""ctypes.CDLL +""" + +EXTERNAL_DEPENDENCIES = [] +"""List of instances of ctypes.CDLL. Helpful when freezing. +""" + +# Types +c_ubyte_p = POINTER(c_ubyte) +c_uint_p = POINTER(c_uint) +c_ulong_p = POINTER(c_ulong) +"""unsigned char* type +""" + + +# Defines and enums +@unique +class ZBarSymbol(IntEnum): + NONE = 0 # /**< no symbol decoded */ + PARTIAL = 1 # /**< intermediate status */ + EAN2 = 2 # /**< GS1 2-digit add-on */ + EAN5 = 5 # /**< GS1 5-digit add-on */ + EAN8 = 8 # /**< EAN-8 */ + UPCE = 9 # /**< UPC-E */ + ISBN10 = 10 # /**< ISBN-10 (from EAN-13). @since 0.4 */ + UPCA = 12 # /**< UPC-A */ + EAN13 = 13 # /**< EAN-13 */ + ISBN13 = 14 # /**< ISBN-13 (from EAN-13). @since 0.4 */ + COMPOSITE = 15 # /**< EAN/UPC composite */ + I25 = 25 # /**< Interleaved 2 of 5. @since 0.4 */ + DATABAR = 34 # /**< GS1 DataBar (RSS). @since 0.11 */ + DATABAR_EXP = 35 # /**< GS1 DataBar Expanded. @since 0.11 */ + CODABAR = 38 # /**< Codabar. @since 0.11 */ + CODE39 = 39 # /**< Code 39. @since 0.4 */ + PDF417 = 57 # /**< PDF417. @since 0.6 */ + QRCODE = 64 # /**< QR Code. @since 0.10 */ + SQCODE = 80 # /**< SQ Code. @since 0.20.1 */ + CODE93 = 93 # /**< Code 93. @since 0.11 */ + CODE128 = 128 # /**< Code 128 */ + + +@unique +class ZBarConfig(IntEnum): + CFG_ENABLE = 0 # /**< enable symbology/feature */ + CFG_ADD_CHECK = 1 # /**< enable check digit when optional */ + CFG_EMIT_CHECK = 2 # /**< return check digit when present */ + CFG_ASCII = 3 # /**< enable full ASCII character set */ + CFG_NUM = 4 # /**< number of boolean decoder configs */ + + CFG_MIN_LEN = 0x20 # /**< minimum data length for valid decode */ + CFG_MAX_LEN = 0x21 # /**< maximum data length for valid decode */ + + CFG_UNCERTAINTY = 0x40 # /**< required video consistency frames */ + + CFG_POSITION = 0x80 # /**< enable scanner to collect position data */ + + CFG_X_DENSITY = 0x100 # /**< image scanner vertical scan density */ + CFG_Y_DENSITY = 0x101 # /**< image scanner horizontal scan density */ + + +@unique +class ZBarOrientation(IntEnum): + UNKNOWN = -1 # /**< unable to determine orientation */ + UP = 0 # /**< upright, read left to right */ + RIGHT = 1 # /**< sideways, read top to bottom */ + DOWN = 2 # /**< upside-down, read right to left */ + LEFT = 3 # /**< sideways, read bottom to top */ + + +# Structs +class zbar_image_scanner(Structure): + """Opaque C++ class with private implementation + """ + pass + + +class zbar_image(Structure): + """Opaque C++ class with private implementation + """ + pass + + +class zbar_symbol(Structure): + """Opaque C++ class with private implementation + + The first item in the structure is an integeger value in the ZBarSymbol + enumeration. + """ + _fields_ = [ + ('type', c_int), + ] + + +def load_libzbar(): + """Loads the zbar shared library and its dependencies. + + Populates the globals LIBZBAR and EXTERNAL_DEPENDENCIES. + """ + global LIBZBAR + global EXTERNAL_DEPENDENCIES + if not LIBZBAR: + libzbar, dependencies = zbar_library.load() + LIBZBAR = libzbar + EXTERNAL_DEPENDENCIES = [LIBZBAR] + dependencies + + return LIBZBAR + + +# Function signatures +def zbar_function(fname, restype, *args): + """Returns a foreign function exported by `zbar`. + + Args: + fname (:obj:`str`): Name of the exported function as string. + restype (:obj:): Return type - one of the `ctypes` primitive C data + types. + *args: Arguments - a sequence of `ctypes` primitive C data types. + + Returns: + cddl.CFunctionType: A wrapper around the function. + """ + prototype = CFUNCTYPE(restype, *args) + return prototype((fname, load_libzbar())) + + +zbar_version = zbar_function( + 'zbar_version', + c_int, + c_uint_p, # major, + c_uint_p, # minor +) + +zbar_set_verbosity = zbar_function( + 'zbar_set_verbosity', + None, + c_int +) + +zbar_image_scanner_create = zbar_function( + 'zbar_image_scanner_create', + POINTER(zbar_image_scanner) +) + +zbar_image_scanner_destroy = zbar_function( + 'zbar_image_scanner_destroy', + None, + POINTER(zbar_image_scanner) +) + +zbar_parse_config = zbar_function( + 'zbar_parse_config', + c_int, + c_char_p, # config_string, + POINTER(c_int), # symbology - values in ZBarSymbol + POINTER(c_int), # config - values in ZBarConfig + POINTER(c_int), # value +) + +zbar_image_scanner_set_config = zbar_function( + 'zbar_image_scanner_set_config', + c_int, + POINTER(zbar_image_scanner), # scanner + c_int, # symbology - values in ZBarSymbol + c_int, # config - values in ZBarConfig + c_int # value +) + +zbar_image_create = zbar_function( + 'zbar_image_create', + POINTER(zbar_image) +) + +zbar_image_destroy = zbar_function( + 'zbar_image_destroy', + None, + POINTER(zbar_image) +) + +zbar_image_set_format = zbar_function( + 'zbar_image_set_format', + None, + POINTER(zbar_image), + c_uint +) + +zbar_image_set_size = zbar_function( + 'zbar_image_set_size', + None, + POINTER(zbar_image), + c_uint, # width + c_uint # height +) + +zbar_image_set_data = zbar_function( + 'zbar_image_set_data', + None, + POINTER(zbar_image), + c_void_p, # data + c_ulong, # raw_image_data_length + c_void_p # A function pointer(!) +) + +zbar_scan_image = zbar_function( + 'zbar_scan_image', + c_int, + POINTER(zbar_image_scanner), + POINTER(zbar_image) +) + +zbar_image_first_symbol = zbar_function( + 'zbar_image_first_symbol', + POINTER(zbar_symbol), + POINTER(zbar_image) +) + +zbar_symbol_get_data_length = zbar_function( + 'zbar_symbol_get_data_length', + c_uint, + POINTER(zbar_symbol) +) + +zbar_symbol_get_data = zbar_function( + 'zbar_symbol_get_data', + c_ubyte_p, + POINTER(zbar_symbol) +) + +zbar_symbol_get_loc_size = zbar_function( + 'zbar_symbol_get_loc_size', + c_uint, + POINTER(zbar_symbol) +) + +zbar_symbol_get_loc_x = zbar_function( + 'zbar_symbol_get_loc_x', + c_int, + POINTER(zbar_symbol), + c_uint +) + +zbar_symbol_get_loc_y = zbar_function( + 'zbar_symbol_get_loc_y', + c_int, + POINTER(zbar_symbol), + c_uint +) + + +try: + zbar_symbol_get_orientation = zbar_function( + 'zbar_symbol_get_orientation', + c_uint, + POINTER(zbar_symbol) + ) +except AttributeError: + # This function not present in the original pre-20 + zbar_symbol_get_orientation = None + + +zbar_symbol_next = zbar_function( + 'zbar_symbol_next', + POINTER(zbar_symbol), + POINTER(zbar_symbol) +) + +zbar_symbol_get_quality = zbar_function( + 'zbar_symbol_get_quality', + c_int, + POINTER(zbar_symbol) +) diff --git a/pyzbar/pyzbar/zbar_library.py b/pyzbar/pyzbar/zbar_library.py new file mode 100644 index 0000000..4627713 --- /dev/null +++ b/pyzbar/pyzbar/zbar_library.py @@ -0,0 +1,22 @@ +"""Loads the zbar shared library bundled with this package.""" +import platform + +from ctypes import cdll +from pathlib import Path + + +def load(): + lib_dir = Path(__file__).parent / 'install' / 'lib' + + if platform.system() == 'Darwin': + lib_name = 'libzbar.dylib' + else: + lib_name = 'libzbar.so' + + lib_path = lib_dir / lib_name + if not lib_path.exists(): + raise ImportError( + f'Bundled zbar shared library not found at {lib_path}' + ) + + return cdll.LoadLibrary(str(lib_path)), [] diff --git a/pyzbar/setup.py b/pyzbar/setup.py new file mode 100644 index 0000000..c61b241 --- /dev/null +++ b/pyzbar/setup.py @@ -0,0 +1,103 @@ +"""Shim setup.py: downloads pre-built wheels from GitHub Releases at install time.""" +import os +import platform +import time +import zipfile +from io import BytesIO +from urllib.error import URLError +from urllib.request import urlopen + +try: + import tomllib +except ImportError: + import tomli as tomllib + +from setuptools import setup +from setuptools.command.build_py import build_py + +_HERE = os.path.dirname(os.path.abspath(__file__)) +with open(os.path.join(_HERE, "pyproject.toml"), "rb") as _f: + _cfg = tomllib.load(_f) + +REPO_URL = _cfg["tool"]["shim"]["repo_url"] +TAG = _cfg["tool"]["shim"]["tag"] +DATADIR = _cfg["tool"]["shim"]["datadir"] +VERSION = _cfg["project"]["version"] +MODULE = _cfg["project"]["name"].replace("-", "_") + +PLATFORM_MAP = { + ("Linux", "x86_64"): "linux_x86_64", + ("Linux", "aarch64"): "linux_aarch64", + ("Darwin", "arm64"): "macosx_11_0_arm64", +} + + +class InstallPrebuilt(build_py): + def run(self): + module_dir = os.path.join(_HERE, MODULE) + data_dir = os.path.join(module_dir, DATADIR) + + if not os.path.exists(os.path.join(data_dir, "bin")): + key = (platform.system(), platform.machine()) + plat = PLATFORM_MAP.get(key) + if plat is None: + raise RuntimeError(f"unsupported platform: {key}") + + whl_name = f"{MODULE}-{VERSION}-py3-none-{plat}.whl" + url = f"{REPO_URL}/releases/download/{TAG}/{whl_name}" + + print(f"Downloading {url} ...") + for attempt in range(3): + try: + raw = urlopen(url, timeout=60).read() + break + except (URLError, OSError) as e: + if attempt == 2: + raise + wait = 2 ** attempt + print(f"Download failed ({e}), retrying in {wait}s ...") + time.sleep(wait) + + print(f"Extracting {DATADIR} ...") + with zipfile.ZipFile(BytesIO(raw)) as zf: + prefix = f"{MODULE}/{DATADIR}/" + alt_prefix = f"{MODULE}-{VERSION}.data/purelib/{MODULE}/{DATADIR}/" + for info in zf.infolist(): + for p in (prefix, alt_prefix): + if info.filename.startswith(p): + rel = info.filename[len(p):] + if not rel: + continue + dest = os.path.join(data_dir, rel) + if info.is_dir(): + os.makedirs(dest, exist_ok=True) + else: + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "wb") as f: + f.write(zf.read(info)) + if info.external_attr >> 16 & 0o111: + os.chmod(dest, 0o755) + break + + # Also extract compiled extension modules (.so) + ext_prefix = f"{MODULE}/" + ext_alt_prefix = f"{MODULE}-{VERSION}.data/purelib/{MODULE}/" + for info in zf.infolist(): + if not info.filename.endswith('.so'): + continue + for p in (ext_prefix, ext_alt_prefix): + if info.filename.startswith(p): + rel = info.filename[len(p):] + if rel and '/' not in rel: + dest = os.path.join(module_dir, rel) + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "wb") as f: + f.write(zf.read(info)) + if info.external_attr >> 16 & 0o111: + os.chmod(dest, 0o755) + break + + super().run() + + +setup(cmdclass={"build_py": InstallPrebuilt})