commit f630d06df85dbea193207c759ae326ccb740e86d Author: MSVSphere Packaging Team Date: Fri Oct 25 18:51:52 2024 +0300 import python-rpm-macros-3.12-8.1.el10 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.python-rpm-macros.metadata b/.python-rpm-macros.metadata new file mode 100644 index 0000000..e69de29 diff --git a/SOURCES/brp-fix-pyc-reproducibility b/SOURCES/brp-fix-pyc-reproducibility new file mode 100644 index 0000000..536a126 --- /dev/null +++ b/SOURCES/brp-fix-pyc-reproducibility @@ -0,0 +1,20 @@ +#!/bin/bash -e + +# If using normal root, avoid changing anything. +if [ -z "$RPM_BUILD_ROOT" -o "$RPM_BUILD_ROOT" = "/" ]; then + exit 0 +fi + +# Defined as %py_reproducible_pyc_path macro and passed here as +# the first command-line argument +path_to_fix=$1 + +# First, check that the parser is available: +if [ ! -x /usr/bin/marshalparser ]; then + echo "ERROR: If %py_reproducible_pyc_path is defined, you have to also BuildRequire: /usr/bin/marshalparser !" + exit 1 +fi + +# Set pipefail so if $path_to_fix does not exist, the build fails +set -o pipefail +find "$path_to_fix" -type f -name "*.pyc" | xargs /usr/bin/marshalparser --fix --overwrite diff --git a/SOURCES/brp-python-bytecompile b/SOURCES/brp-python-bytecompile new file mode 100644 index 0000000..1f7c4cd --- /dev/null +++ b/SOURCES/brp-python-bytecompile @@ -0,0 +1,157 @@ +#!/bin/bash +errors_terminate=$2 + +# Usage of %_python_bytecompile_extra is not allowed anymore +# See: https://fedoraproject.org/wiki/Changes/No_more_automagic_Python_bytecompilation_phase_3 +# Therefore $1 ($default_python) is not needed and is invoked with "" by default. +# $default_python stays in the arguments for backward compatibility and $extra for the following check: +extra=$3 +if [ 0$extra -eq 1 ]; then + echo -e "%_python_bytecompile_extra is discontinued, use %py_byte_compile instead.\nSee: https://fedoraproject.org/wiki/Changes/No_more_automagic_Python_bytecompilation_phase_3" >/dev/stderr + exit 1 +fi + +compileall_flags="$4" + +# If using normal root, avoid changing anything. +if [ -z "$RPM_BUILD_ROOT" -o "$RPM_BUILD_ROOT" = "/" ]; then + exit 0 +fi + +# This function clamps the source mtime, see https://fedoraproject.org/wiki/Changes/ReproducibleBuildsClampMtimes +function python_clamp_source_mtime() +{ + local _=$1 + local python_binary=$2 + local _=$3 + local python_libdir="$4" + PYTHONPATH=/usr/lib/rpm/redhat/ $python_binary -B -m clamp_source_mtime -q "$python_libdir" +} + +# This function now implements Python byte-compilation in three different ways: +# Python >= 3.4 and < 3.9 uses a new module compileall2 - https://github.com/fedora-python/compileall2 +# In Python >= 3.9, compileall2 was merged back to standard library (compileall) so we can use it directly again. +# Python < 3.4 (inc. Python 2) uses compileall module from stdlib with some hacks +function python_bytecompile() +{ + local options=$1 + local python_binary=$2 + local exclude=$3 + local python_libdir="$4" + local compileall_flags="$5" + + python_version=$($python_binary -c "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))") + + # + # Python 3.4 and higher + # + if [ "$python_version" -ge 34 ]; then + + # We compile all opt levels in one go: only when $options is empty. + if [ -n "$options" ]; then + return + fi + + if [ "$python_version" -ge 39 ]; then + # For Pyhon 3.9+, use the standard library + compileall_module=compileall + else + # For older Pythons, use compileall2 + compileall_module=compileall2 + fi + + if [ "$python_version" -ge 37 ]; then + # Force the TIMESTAMP invalidation mode + invalidation_option=--invalidation-mode=timestamp + else + # For older Pythons, the option does not exist + # as the invalidation is always based on size+mtime + invalidation_option= + fi + + [ ! -z $exclude ] && exclude="-x '$exclude'" + + # PYTHONPATH is needed for compileall2, but doesn't hurt for the stdlib + # -o 0 -o 1 are the optimization levels + # -q disables verbose output + # -f forces the process to overwrite existing compiled files + # -x excludes paths defined by regex + # -e excludes symbolic links pointing outside the build root + # -x and -e together implements the same functionality as the Filter class below + # -s strips $RPM_BUILD_ROOT from the path + # -p prepends the leading slash to the path to make it absolute + PYTHONPATH=/usr/lib/rpm/redhat/ $python_binary -B -m $compileall_module $compileall_flags -o 0 -o 1 -q -f $exclude -s "$RPM_BUILD_ROOT" -p / --hardlink-dupes $invalidation_option -e "$RPM_BUILD_ROOT" "$python_libdir" + + else +# +# Python 3.3 and lower (incl. Python 2) +# + +local real_libdir=${python_libdir/$RPM_BUILD_ROOT/} + +cat << EOF | $python_binary $options +import compileall, sys, os, re + +python_libdir = "$python_libdir" +depth = sys.getrecursionlimit() +real_libdir = "$real_libdir" +build_root = "$RPM_BUILD_ROOT" +exclude = r"$exclude" + +class Filter: + def search(self, path): + ret = not os.path.realpath(path).startswith(build_root) + if exclude: + ret = ret or re.search(exclude, path) + return ret + +sys.exit(not compileall.compile_dir(python_libdir, depth, real_libdir, force=1, rx=Filter(), quiet=1)) +EOF + +fi +} + +# .pyc/.pyo files embed a "magic" value, identifying the ABI version of Python +# bytecode that they are for. +# +# The files below RPM_BUILD_ROOT could be targeting multiple versions of +# python (e.g. a single build that emits several subpackages e.g. a +# python26-foo subpackage, a python31-foo subpackage etc) +# +# Support this by assuming that below each /usr/lib/python$VERSION/, all +# .pyc/.pyo files are to be compiled for /usr/bin/python$VERSION. +# +# For example, below /usr/lib/python2.6/, we're targeting /usr/bin/python2.6 +# and below /usr/lib/python3.1/, we're targeting /usr/bin/python3.1 + +# Disable Python hash seed randomization +# This should help with byte-compilation reproducibility: https://bugzilla.redhat.com/show_bug.cgi?id=1686078 +# Python 3.11+ no longer needs this: https://github.com/python/cpython/pull/27926 (but we support older Pythons as well) +export PYTHONHASHSEED=0 + +shopt -s nullglob +find "$RPM_BUILD_ROOT" -type d -print0|grep -z -E "/(usr|app)/lib(64)?/python[0-9]\.[0-9]+$" | while read -d "" python_libdir; +do + python_binary=$(basename "$python_libdir") + echo "Bytecompiling .py files below $python_libdir using $python_binary" + + # Generate normal (.pyc) byte-compiled files. + python_clamp_source_mtime "" "$python_binary" "" "$python_libdir" "" + if [ $? -ne 0 -a 0$errors_terminate -ne 0 ]; then + # One or more of the files had inaccessible mtime + exit 1 + fi + python_bytecompile "" "$python_binary" "" "$python_libdir" "$compileall_flags" + if [ $? -ne 0 -a 0$errors_terminate -ne 0 ]; then + # One or more of the files had a syntax error + exit 1 + fi + + # Generate optimized (.pyo) byte-compiled files. + # N.B. For Python 3.4+, this call does nothing + python_bytecompile "-O" "$python_binary" "" "$python_libdir" "$compileall_flags" + if [ $? -ne 0 -a 0$errors_terminate -ne 0 ]; then + # One or more of the files had a syntax error + exit 1 + fi +done diff --git a/SOURCES/brp-python-hardlink b/SOURCES/brp-python-hardlink new file mode 100644 index 0000000..5fd1b43 --- /dev/null +++ b/SOURCES/brp-python-hardlink @@ -0,0 +1,25 @@ +#!/bin/sh + +# If using normal root, avoid changing anything. +if [ -z "$RPM_BUILD_ROOT" ] || [ "$RPM_BUILD_ROOT" = "/" ]; then + exit 0 +fi + +hardlink_if_same() { + if cmp -s "$1" "$2" ; then + ln -f "$1" "$2" + return 0 + fi + return 1 +} + +# Hardlink identical *.pyc, *.pyo, and *.opt-[12].pyc. +# Originally from PLD's rpm-build-macros +find "$RPM_BUILD_ROOT" -type f -name "*.pyc" -not -name "*.opt-[12].pyc" | while read pyc ; do + hardlink_if_same "$pyc" "${pyc%c}o" + o1pyc="${pyc%pyc}opt-1.pyc" + hardlink_if_same "$pyc" "$o1pyc" + o2pyc="${pyc%pyc}opt-2.pyc" + hardlink_if_same "$pyc" "$o2pyc" || hardlink_if_same "$o1pyc" "$o2pyc" +done +exit 0 diff --git a/SOURCES/clamp_source_mtime.py b/SOURCES/clamp_source_mtime.py new file mode 100644 index 0000000..1d03a6b --- /dev/null +++ b/SOURCES/clamp_source_mtime.py @@ -0,0 +1,163 @@ +"""Module/script to clamp the mtimes of all .py files to $SOURCE_DATE_EPOCH + +When called as a script with arguments, this compiles the directories +given as arguments recursively. + +If upstream is interested, this can be later integrated to the compileall module +as an additional option (e.g. --clamp-source-mtime). + +License: +This has been derived from the Python's compileall module +and it follows Python licensing. For more info see: https://www.python.org/psf/license/ +""" +from __future__ import print_function +import os +import sys + +# Python 3.6 and higher +PY36 = sys.version_info[0:2] >= (3, 6) + +__all__ = ["clamp_dir", "clamp_file"] + + +def _walk_dir(dir, maxlevels, quiet=0): + if PY36 and quiet < 2 and isinstance(dir, os.PathLike): + dir = os.fspath(dir) + else: + dir = str(dir) + if not quiet: + print('Listing {!r}...'.format(dir)) + try: + names = os.listdir(dir) + except OSError: + if quiet < 2: + print("Can't list {!r}".format(dir)) + names = [] + names.sort() + for name in names: + if name == '__pycache__': + continue + fullname = os.path.join(dir, name) + if not os.path.isdir(fullname): + yield fullname + elif (maxlevels > 0 and name != os.curdir and name != os.pardir and + os.path.isdir(fullname) and not os.path.islink(fullname)): + for result in _walk_dir(fullname, maxlevels=maxlevels - 1, + quiet=quiet): + yield result + + +def clamp_dir(dir, source_date_epoch, quiet=0): + """Clamp the mtime of all modules in the given directory tree. + + Arguments: + + dir: the directory to byte-compile + source_date_epoch: integer parsed from $SOURCE_DATE_EPOCH + quiet: full output with False or 0, errors only with 1, + no output with 2 + """ + maxlevels = sys.getrecursionlimit() + files = _walk_dir(dir, quiet=quiet, maxlevels=maxlevels) + success = True + for file in files: + if not clamp_file(file, source_date_epoch, quiet=quiet): + success = False + return success + + +def clamp_file(fullname, source_date_epoch, quiet=0): + """Clamp the mtime of one file. + + Arguments: + + fullname: the file to byte-compile + source_date_epoch: integer parsed from $SOURCE_DATE_EPOCH + quiet: full output with False or 0, errors only with 1, + no output with 2 + """ + if PY36 and quiet < 2 and isinstance(fullname, os.PathLike): + fullname = os.fspath(fullname) + else: + fullname = str(fullname) + name = os.path.basename(fullname) + + if os.path.isfile(fullname) and not os.path.islink(fullname): + if name[-3:] == '.py': + try: + mtime = int(os.stat(fullname).st_mtime) + atime = int(os.stat(fullname).st_atime) + except OSError as e: + if quiet >= 2: + return False + elif quiet: + print('*** Error checking mtime of {!r}...'.format(fullname)) + else: + print('*** ', end='') + print(e.__class__.__name__ + ':', e) + return False + if mtime > source_date_epoch: + if not quiet: + print('Clamping mtime of {!r}'.format(fullname)) + try: + os.utime(fullname, (atime, source_date_epoch)) + except OSError as e: + if quiet >= 2: + return False + elif quiet: + print('*** Error clamping mtime of {!r}...'.format(fullname)) + else: + print('*** ', end='') + print(e.__class__.__name__ + ':', e) + return False + return True + + +def main(): + """Script main program.""" + import argparse + + source_date_epoch = os.getenv('SOURCE_DATE_EPOCH') + if not source_date_epoch: + print("Not clamping source mtimes, $SOURCE_DATE_EPOCH not set") + return True # This is a success, no action needed + try: + source_date_epoch = int(source_date_epoch) + except ValueError: + print("$SOURCE_DATE_EPOCH must be an integer") + return False + + parser = argparse.ArgumentParser( + description='Clamp .py source mtime to $SOURCE_DATE_EPOCH.') + parser.add_argument('-q', action='count', dest='quiet', default=0, + help='output only error messages; -qq will suppress ' + 'the error messages as well.') + parser.add_argument('clamp_dest', metavar='FILE|DIR', nargs='+', + help=('zero or more file and directory paths ' + 'to clamp')) + + args = parser.parse_args() + clamp_dests = args.clamp_dest + + success = True + try: + for dest in clamp_dests: + if os.path.isfile(dest): + if not clamp_file(dest, quiet=args.quiet, + source_date_epoch=source_date_epoch): + success = False + else: + if not clamp_dir(dest, quiet=args.quiet, + source_date_epoch=source_date_epoch): + success = False + return success + except KeyboardInterrupt: + if args.quiet < 2: + print("\n[interrupted]") + return False + return True + + +if __name__ == '__main__': + exit_status = int(not main()) + sys.exit(exit_status) diff --git a/SOURCES/compileall2.py b/SOURCES/compileall2.py new file mode 100644 index 0000000..c58e545 --- /dev/null +++ b/SOURCES/compileall2.py @@ -0,0 +1,515 @@ +"""Module/script to byte-compile all .py files to .pyc files. + +When called as a script with arguments, this compiles the directories +given as arguments recursively; the -l option prevents it from +recursing into directories. + +Without arguments, if compiles all modules on sys.path, without +recursing into subdirectories. (Even though it should do so for +packages -- for now, you'll have to deal with packages separately.) + +See module py_compile for details of the actual byte-compilation. + +License: +Compileall2 is an enhanced copy of Python's compileall module +and it follows Python licensing. For more info see: https://www.python.org/psf/license/ +""" +import os +import sys +import importlib.util +import py_compile +import struct +import filecmp + +from functools import partial +from pathlib import Path + +# Python 3.7 and higher +PY37 = sys.version_info[0:2] >= (3, 7) +# Python 3.6 and higher +PY36 = sys.version_info[0:2] >= (3, 6) +# Python 3.5 and higher +PY35 = sys.version_info[0:2] >= (3, 5) + +# Python 3.7 and above has a different structure and length +# of pyc files header. Also, multiple ways how to invalidate pyc file was +# introduced in Python 3.7. These cases are covered by variables here or by PY37 +# variable itself. +if PY37: + pyc_struct_format = '<4sll' + pyc_header_lenght = 12 + pyc_header_format = (pyc_struct_format, importlib.util.MAGIC_NUMBER, 0) +else: + pyc_struct_format = '<4sl' + pyc_header_lenght = 8 + pyc_header_format = (pyc_struct_format, importlib.util.MAGIC_NUMBER) + +__all__ = ["compile_dir","compile_file","compile_path"] + +def optimization_kwarg(opt): + """Returns opt as a dictionary {optimization: opt} for use as **kwarg + for Python >= 3.5 and empty dictionary for Python 3.4""" + if PY35: + return dict(optimization=opt) + else: + # `debug_override` is a way how to enable optimized byte-compiled files + # (.pyo) in Python <= 3.4 + if opt: + return dict(debug_override=False) + else: + return dict() + +def _walk_dir(dir, maxlevels, quiet=0): + if PY36 and quiet < 2 and isinstance(dir, os.PathLike): + dir = os.fspath(dir) + else: + dir = str(dir) + if not quiet: + print('Listing {!r}...'.format(dir)) + try: + names = os.listdir(dir) + except OSError: + if quiet < 2: + print("Can't list {!r}".format(dir)) + names = [] + names.sort() + for name in names: + if name == '__pycache__': + continue + fullname = os.path.join(dir, name) + if not os.path.isdir(fullname): + yield fullname + elif (maxlevels > 0 and name != os.curdir and name != os.pardir and + os.path.isdir(fullname) and not os.path.islink(fullname)): + yield from _walk_dir(fullname, maxlevels=maxlevels - 1, + quiet=quiet) + +def compile_dir(dir, maxlevels=None, ddir=None, force=False, + rx=None, quiet=0, legacy=False, optimize=-1, workers=1, + invalidation_mode=None, stripdir=None, + prependdir=None, limit_sl_dest=None, hardlink_dupes=False): + """Byte-compile all modules in the given directory tree. + + Arguments (only dir is required): + + dir: the directory to byte-compile + maxlevels: maximum recursion level (default `sys.getrecursionlimit()`) + ddir: the directory that will be prepended to the path to the + file as it is compiled into each byte-code file. + force: if True, force compilation, even if timestamps are up-to-date + quiet: full output with False or 0, errors only with 1, + no output with 2 + legacy: if True, produce legacy pyc paths instead of PEP 3147 paths + optimize: int or list of optimization levels or -1 for level of + the interpreter. Multiple levels leads to multiple compiled + files each with one optimization level. + workers: maximum number of parallel workers + invalidation_mode: how the up-to-dateness of the pyc will be checked + stripdir: part of path to left-strip from source file path + prependdir: path to prepend to beggining of original file path, applied + after stripdir + limit_sl_dest: ignore symlinks if they are pointing outside of + the defined path + hardlink_dupes: hardlink duplicated pyc files + """ + ProcessPoolExecutor = None + if ddir is not None and (stripdir is not None or prependdir is not None): + raise ValueError(("Destination dir (ddir) cannot be used " + "in combination with stripdir or prependdir")) + if ddir is not None: + stripdir = dir + prependdir = ddir + ddir = None + if workers is not None: + if workers < 0: + raise ValueError('workers must be greater or equal to 0') + elif workers != 1: + try: + # Only import when needed, as low resource platforms may + # fail to import it + from concurrent.futures import ProcessPoolExecutor + except ImportError: + workers = 1 + if maxlevels is None: + maxlevels = sys.getrecursionlimit() + files = _walk_dir(dir, quiet=quiet, maxlevels=maxlevels) + success = True + if workers is not None and workers != 1 and ProcessPoolExecutor is not None: + workers = workers or None + with ProcessPoolExecutor(max_workers=workers) as executor: + results = executor.map(partial(compile_file, + ddir=ddir, force=force, + rx=rx, quiet=quiet, + legacy=legacy, + optimize=optimize, + invalidation_mode=invalidation_mode, + stripdir=stripdir, + prependdir=prependdir, + limit_sl_dest=limit_sl_dest), + files) + success = min(results, default=True) + else: + for file in files: + if not compile_file(file, ddir, force, rx, quiet, + legacy, optimize, invalidation_mode, + stripdir=stripdir, prependdir=prependdir, + limit_sl_dest=limit_sl_dest, + hardlink_dupes=hardlink_dupes): + success = False + return success + +def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0, + legacy=False, optimize=-1, + invalidation_mode=None, stripdir=None, prependdir=None, + limit_sl_dest=None, hardlink_dupes=False): + """Byte-compile one file. + + Arguments (only fullname is required): + + fullname: the file to byte-compile + ddir: if given, the directory name compiled in to the + byte-code file. + force: if True, force compilation, even if timestamps are up-to-date + quiet: full output with False or 0, errors only with 1, + no output with 2 + legacy: if True, produce legacy pyc paths instead of PEP 3147 paths + optimize: int or list of optimization levels or -1 for level of + the interpreter. Multiple levels leads to multiple compiled + files each with one optimization level. + invalidation_mode: how the up-to-dateness of the pyc will be checked + stripdir: part of path to left-strip from source file path + prependdir: path to prepend to beggining of original file path, applied + after stripdir + limit_sl_dest: ignore symlinks if they are pointing outside of + the defined path. + hardlink_dupes: hardlink duplicated pyc files + """ + + if ddir is not None and (stripdir is not None or prependdir is not None): + raise ValueError(("Destination dir (ddir) cannot be used " + "in combination with stripdir or prependdir")) + + success = True + if PY36 and quiet < 2 and isinstance(fullname, os.PathLike): + fullname = os.fspath(fullname) + else: + fullname = str(fullname) + name = os.path.basename(fullname) + + dfile = None + + if ddir is not None: + if not PY36: + ddir = str(ddir) + dfile = os.path.join(ddir, name) + + if stripdir is not None: + fullname_parts = fullname.split(os.path.sep) + stripdir_parts = stripdir.split(os.path.sep) + ddir_parts = list(fullname_parts) + + for spart, opart in zip(stripdir_parts, fullname_parts): + if spart == opart: + ddir_parts.remove(spart) + + dfile = os.path.join(*ddir_parts) + + if prependdir is not None: + if dfile is None: + dfile = os.path.join(prependdir, fullname) + else: + dfile = os.path.join(prependdir, dfile) + + if isinstance(optimize, int): + optimize = [optimize] + + if hardlink_dupes: + raise ValueError(("Hardlinking of duplicated bytecode makes sense " + "only for more than one optimization level.")) + + if rx is not None: + mo = rx.search(fullname) + if mo: + return success + + if limit_sl_dest is not None and os.path.islink(fullname): + if Path(limit_sl_dest).resolve() not in Path(fullname).resolve().parents: + return success + + opt_cfiles = {} + + if os.path.isfile(fullname): + for opt_level in optimize: + if legacy: + opt_cfiles[opt_level] = fullname + 'c' + else: + if opt_level >= 0: + opt = opt_level if opt_level >= 1 else '' + opt_kwarg = optimization_kwarg(opt) + cfile = (importlib.util.cache_from_source( + fullname, **opt_kwarg)) + opt_cfiles[opt_level] = cfile + else: + cfile = importlib.util.cache_from_source(fullname) + opt_cfiles[opt_level] = cfile + + head, tail = name[:-3], name[-3:] + if tail == '.py': + if not force: + try: + mtime = int(os.stat(fullname).st_mtime) + expect = struct.pack(*(pyc_header_format + (mtime,))) + for cfile in opt_cfiles.values(): + with open(cfile, 'rb') as chandle: + actual = chandle.read(pyc_header_lenght) + if expect != actual: + break + else: + return success + except OSError: + pass + if not quiet: + print('Compiling {!r}...'.format(fullname)) + try: + for index, opt_level in enumerate(sorted(optimize)): + cfile = opt_cfiles[opt_level] + if PY37: + ok = py_compile.compile(fullname, cfile, dfile, True, + optimize=opt_level, + invalidation_mode=invalidation_mode) + else: + ok = py_compile.compile(fullname, cfile, dfile, True, + optimize=opt_level) + + if index > 0 and hardlink_dupes: + previous_cfile = opt_cfiles[optimize[index - 1]] + if previous_cfile == cfile and optimize[0] not in (1, 2): + # Python 3.4 has only one .pyo file for -O and -OO so + # we hardlink it only if there is a .pyc file + # with the same content + previous_cfile = opt_cfiles[optimize[0]] + if previous_cfile != cfile and filecmp.cmp(cfile, previous_cfile, shallow=False): + os.unlink(cfile) + os.link(previous_cfile, cfile) + + except py_compile.PyCompileError as err: + success = False + if quiet >= 2: + return success + elif quiet: + print('*** Error compiling {!r}...'.format(fullname)) + else: + print('*** ', end='') + # escape non-printable characters in msg + msg = err.msg.encode(sys.stdout.encoding, + errors='backslashreplace') + msg = msg.decode(sys.stdout.encoding) + print(msg) + except (SyntaxError, UnicodeError, OSError) as e: + success = False + if quiet >= 2: + return success + elif quiet: + print('*** Error compiling {!r}...'.format(fullname)) + else: + print('*** ', end='') + print(e.__class__.__name__ + ':', e) + else: + if ok == 0: + success = False + return success + +def compile_path(skip_curdir=1, maxlevels=0, force=False, quiet=0, + legacy=False, optimize=-1, + invalidation_mode=None): + """Byte-compile all module on sys.path. + + Arguments (all optional): + + skip_curdir: if true, skip current directory (default True) + maxlevels: max recursion level (default 0) + force: as for compile_dir() (default False) + quiet: as for compile_dir() (default 0) + legacy: as for compile_dir() (default False) + optimize: as for compile_dir() (default -1) + invalidation_mode: as for compiler_dir() + """ + success = True + for dir in sys.path: + if (not dir or dir == os.curdir) and skip_curdir: + if quiet < 2: + print('Skipping current directory') + else: + success = success and compile_dir( + dir, + maxlevels, + None, + force, + quiet=quiet, + legacy=legacy, + optimize=optimize, + invalidation_mode=invalidation_mode, + ) + return success + + +def main(): + """Script main program.""" + import argparse + + parser = argparse.ArgumentParser( + description='Utilities to support installing Python libraries.') + parser.add_argument('-l', action='store_const', const=0, + default=None, dest='maxlevels', + help="don't recurse into subdirectories") + parser.add_argument('-r', type=int, dest='recursion', + help=('control the maximum recursion level. ' + 'if `-l` and `-r` options are specified, ' + 'then `-r` takes precedence.')) + parser.add_argument('-f', action='store_true', dest='force', + help='force rebuild even if timestamps are up to date') + parser.add_argument('-q', action='count', dest='quiet', default=0, + help='output only error messages; -qq will suppress ' + 'the error messages as well.') + parser.add_argument('-b', action='store_true', dest='legacy', + help='use legacy (pre-PEP3147) compiled file locations') + parser.add_argument('-d', metavar='DESTDIR', dest='ddir', default=None, + help=('directory to prepend to file paths for use in ' + 'compile-time tracebacks and in runtime ' + 'tracebacks in cases where the source file is ' + 'unavailable')) + parser.add_argument('-s', metavar='STRIPDIR', dest='stripdir', + default=None, + help=('part of path to left-strip from path ' + 'to source file - for example buildroot. ' + '`-d` and `-s` options cannot be ' + 'specified together.')) + parser.add_argument('-p', metavar='PREPENDDIR', dest='prependdir', + default=None, + help=('path to add as prefix to path ' + 'to source file - for example / to make ' + 'it absolute when some part is removed ' + 'by `-s` option. ' + '`-d` and `-p` options cannot be ' + 'specified together.')) + parser.add_argument('-x', metavar='REGEXP', dest='rx', default=None, + help=('skip files matching the regular expression; ' + 'the regexp is searched for in the full path ' + 'of each file considered for compilation')) + parser.add_argument('-i', metavar='FILE', dest='flist', + help=('add all the files and directories listed in ' + 'FILE to the list considered for compilation; ' + 'if "-", names are read from stdin')) + parser.add_argument('compile_dest', metavar='FILE|DIR', nargs='*', + help=('zero or more file and directory names ' + 'to compile; if no arguments given, defaults ' + 'to the equivalent of -l sys.path')) + parser.add_argument('-j', '--workers', default=1, + type=int, help='Run compileall concurrently') + parser.add_argument('-o', action='append', type=int, dest='opt_levels', + help=('Optimization levels to run compilation with. ' + 'Default is -1 which uses optimization level of ' + 'Python interpreter itself (specified by -O).')) + parser.add_argument('-e', metavar='DIR', dest='limit_sl_dest', + help='Ignore symlinks pointing outsite of the DIR') + parser.add_argument('--hardlink-dupes', action='store_true', + dest='hardlink_dupes', + help='Hardlink duplicated pyc files') + + if PY37: + invalidation_modes = [mode.name.lower().replace('_', '-') + for mode in py_compile.PycInvalidationMode] + parser.add_argument('--invalidation-mode', + choices=sorted(invalidation_modes), + help=('set .pyc invalidation mode; defaults to ' + '"checked-hash" if the SOURCE_DATE_EPOCH ' + 'environment variable is set, and ' + '"timestamp" otherwise.')) + + args = parser.parse_args() + compile_dests = args.compile_dest + + if args.rx: + import re + args.rx = re.compile(args.rx) + + if args.limit_sl_dest == "": + args.limit_sl_dest = None + + if args.recursion is not None: + maxlevels = args.recursion + else: + maxlevels = args.maxlevels + + if args.opt_levels is None: + args.opt_levels = [-1] + + if len(args.opt_levels) == 1 and args.hardlink_dupes: + parser.error(("Hardlinking of duplicated bytecode makes sense " + "only for more than one optimization level.")) + + if args.ddir is not None and ( + args.stripdir is not None or args.prependdir is not None + ): + parser.error("-d cannot be used in combination with -s or -p") + + # if flist is provided then load it + if args.flist: + try: + with (sys.stdin if args.flist=='-' else open(args.flist)) as f: + for line in f: + compile_dests.append(line.strip()) + except OSError: + if args.quiet < 2: + print("Error reading file list {}".format(args.flist)) + return False + + if args.workers is not None: + args.workers = args.workers or None + + if PY37 and args.invalidation_mode: + ivl_mode = args.invalidation_mode.replace('-', '_').upper() + invalidation_mode = py_compile.PycInvalidationMode[ivl_mode] + else: + invalidation_mode = None + + success = True + try: + if compile_dests: + for dest in compile_dests: + if os.path.isfile(dest): + if not compile_file(dest, args.ddir, args.force, args.rx, + args.quiet, args.legacy, + invalidation_mode=invalidation_mode, + stripdir=args.stripdir, + prependdir=args.prependdir, + optimize=args.opt_levels, + limit_sl_dest=args.limit_sl_dest, + hardlink_dupes=args.hardlink_dupes): + success = False + else: + if not compile_dir(dest, maxlevels, args.ddir, + args.force, args.rx, args.quiet, + args.legacy, workers=args.workers, + invalidation_mode=invalidation_mode, + stripdir=args.stripdir, + prependdir=args.prependdir, + optimize=args.opt_levels, + limit_sl_dest=args.limit_sl_dest, + hardlink_dupes=args.hardlink_dupes): + success = False + return success + else: + return compile_path(legacy=args.legacy, force=args.force, + quiet=args.quiet, + invalidation_mode=invalidation_mode) + except KeyboardInterrupt: + if args.quiet < 2: + print("\n[interrupted]") + return False + return True + + +if __name__ == '__main__': + exit_status = int(not main()) + sys.exit(exit_status) diff --git a/SOURCES/import_all_modules.py b/SOURCES/import_all_modules.py new file mode 100644 index 0000000..3930236 --- /dev/null +++ b/SOURCES/import_all_modules.py @@ -0,0 +1,171 @@ +'''Script to perform import of each module given to %%py_check_import +''' +import argparse +import importlib +import fnmatch +import os +import re +import site +import sys + +from contextlib import contextmanager +from pathlib import Path + + +def read_modules_files(file_paths): + '''Read module names from the files (modules must be newline separated). + + Return the module names list or, if no files were provided, an empty list. + ''' + + if not file_paths: + return [] + + modules = [] + for file in file_paths: + file_contents = file.read_text() + modules.extend(file_contents.split()) + return modules + + +def read_modules_from_cli(argv): + '''Read module names from command-line arguments (space or comma separated). + + Return the module names list. + ''' + + if not argv: + return [] + + # %%py3_check_import allows to separate module list with comma or whitespace, + # we need to unify the output to a list of particular elements + modules_as_str = ' '.join(argv) + modules = re.split(r'[\s,]+', modules_as_str) + # Because of shell expansion in some less typical cases it may happen + # that a trailing space will occur at the end of the list. + # Remove the empty items from the list before passing it further + modules = [m for m in modules if m] + return modules + + +def filter_top_level_modules_only(modules): + '''Filter out entries with nested modules (containing dot) ie. 'foo.bar'. + + Return the list of top-level modules. + ''' + + return [module for module in modules if '.' not in module] + + +def any_match(text, globs): + '''Return True if any of given globs fnmatchcase's the given text.''' + + return any(fnmatch.fnmatchcase(text, g) for g in globs) + + +def exclude_unwanted_module_globs(globs, modules): + '''Filter out entries which match the either of the globs given as argv. + + Return the list of filtered modules. + ''' + + return [m for m in modules if not any_match(m, globs)] + + +def read_modules_from_all_args(args): + '''Return a joined list of modules from all given command-line arguments. + ''' + + modules = read_modules_files(args.filename) + modules.extend(read_modules_from_cli(args.modules)) + if args.exclude: + modules = exclude_unwanted_module_globs(args.exclude, modules) + + if args.top_level: + modules = filter_top_level_modules_only(modules) + + # Error when someone accidentally managed to filter out everything + if len(modules) == 0: + raise ValueError('No modules to check were left') + + return modules + + +def import_modules(modules): + '''Procedure to perform import check for each module name from the given list of modules. + ''' + + for module in modules: + print('Check import:', module, file=sys.stderr) + importlib.import_module(module) + + +def argparser(): + parser = argparse.ArgumentParser( + description='Generate list of all importable modules for import check.' + ) + parser.add_argument( + 'modules', nargs='*', + help=('Add modules to check the import (space or comma separated).'), + ) + parser.add_argument( + '-f', '--filename', action='append', type=Path, + help='Add importable module names list from file.', + ) + parser.add_argument( + '-t', '--top-level', action='store_true', + help='Check only top-level modules.', + ) + parser.add_argument( + '-e', '--exclude', action='append', + help='Provide modules globs to be excluded from the check.', + ) + return parser + + +@contextmanager +def remove_unwanteds_from_sys_path(): + '''Remove cwd and this script's parent from sys.path for the import test. + Bring the original contents back after import is done (or failed) + ''' + + cwd_absolute = Path.cwd().absolute() + this_file_parent = Path(__file__).parent.absolute() + old_sys_path = list(sys.path) + for path in old_sys_path: + if Path(path).absolute() in (cwd_absolute, this_file_parent): + sys.path.remove(path) + try: + yield + finally: + sys.path = old_sys_path + + +def addsitedirs_from_environ(): + '''Load directories from the _PYTHONSITE environment variable (separated by :) + and load the ones already present in sys.path via site.addsitedir() + to handle .pth files in them. + + This is needed to properly import old-style namespace packages with nspkg.pth files. + See https://bugzilla.redhat.com/2018551 for a more detailed rationale.''' + for path in os.getenv('_PYTHONSITE', '').split(':'): + if path in sys.path: + site.addsitedir(path) + + +def main(argv=None): + + cli_args = argparser().parse_args(argv) + + if not cli_args.modules and not cli_args.filename: + raise ValueError('No modules to check were provided') + + modules = read_modules_from_all_args(cli_args) + + with remove_unwanteds_from_sys_path(): + addsitedirs_from_environ() + import_modules(modules) + + +if __name__ == '__main__': + main() diff --git a/SOURCES/macros.pybytecompile b/SOURCES/macros.pybytecompile new file mode 100644 index 0000000..e81ddae --- /dev/null +++ b/SOURCES/macros.pybytecompile @@ -0,0 +1,69 @@ +# Note that the path could itself be a python file, or a directory + +# Note that the py_byte_compile macro should work for all Python versions +# Which unfortunately makes the definition more complicated than it should be + +# Usage: +# %%py_byte_compile +# Example: +# %%py_byte_compile %%{__python3} %%{buildroot}%%{_datadir}/spam/plugins/ + +# This will terminate build on SyntaxErrors, if you want to avoid that, +# use it in a subshell like this: +# (%%{py_byte_compile }) || : + +# Setting PYTHONHASHSEED=0 disables Python hash seed randomization +# This should help with byte-compilation reproducibility: https://bugzilla.redhat.com/show_bug.cgi?id=1686078 +# Python 3.11+ no longer needs this: https://github.com/python/cpython/pull/27926 (but we support older Pythons as well) + +%py_byte_compile()\ +clamp_source_mtime () {\ + python_binary="%{__env_unset_source_date_epoch_if_not_clamp_mtime} %1"\ + bytecode_compilation_path="%2"\ + PYTHONPATH="%{_rpmconfigdir}/redhat" $python_binary -s -B -m clamp_source_mtime $bytecode_compilation_path \ +}\ +\ +py2_byte_compile () {\ + python_binary="%{__env_unset_source_date_epoch_if_not_clamp_mtime} PYTHONHASHSEED=0 %1"\ + bytecode_compilation_path="%2"\ + failure=0\ + find $bytecode_compilation_path -type f -a -name "*.py" -print0 | xargs -0 $python_binary -s -c 'import py_compile, sys; [py_compile.compile(f, dfile=f.partition("'"$RPM_BUILD_ROOT"'")[2], doraise=True) for f in sys.argv[1:]]' || failure=1\ + find $bytecode_compilation_path -type f -a -name "*.py" -print0 | xargs -0 $python_binary -s -O -c 'import py_compile, sys; [py_compile.compile(f, dfile=f.partition("'"$RPM_BUILD_ROOT"'")[2], doraise=True) for f in sys.argv[1:]]' || failure=1\ + test $failure -eq 0\ +}\ +\ +py34_byte_compile () {\ + python_binary="%{__env_unset_source_date_epoch_if_not_clamp_mtime} PYTHONHASHSEED=0 %1"\ + bytecode_compilation_path="%2"\ + PYTHONPATH="%{_rpmconfigdir}/redhat" $python_binary -s -B -m compileall2 %{?_smp_build_ncpus:-j%{_smp_build_ncpus}} -o 0 -o 1 -s $RPM_BUILD_ROOT -p / --hardlink-dupes $bytecode_compilation_path \ +}\ +py37_byte_compile () {\ + python_binary="%{__env_unset_source_date_epoch_if_not_clamp_mtime} PYTHONHASHSEED=0 %1"\ + bytecode_compilation_path="%2"\ + PYTHONPATH="%{_rpmconfigdir}/redhat" $python_binary -s -B -m compileall2 %{?_smp_build_ncpus:-j%{_smp_build_ncpus}} -o 0 -o 1 -s $RPM_BUILD_ROOT -p / --hardlink-dupes --invalidation-mode=timestamp $bytecode_compilation_path \ +}\ +\ +py39_byte_compile () {\ + python_binary="%{__env_unset_source_date_epoch_if_not_clamp_mtime} PYTHONHASHSEED=0 %1"\ + bytecode_compilation_path="%2"\ + $python_binary -s -B -m compileall %{?_smp_build_ncpus:-j%{_smp_build_ncpus}} -o 0 -o 1 -s $RPM_BUILD_ROOT -p / --hardlink-dupes --invalidation-mode=timestamp $bytecode_compilation_path \ +}\ +\ +# Path to intepreter should not contain any arguments \ +[[ "%1" =~ " -" ]] && echo "ERROR py_byte_compile: Path to interpreter should not contain any arguments" >&2 && exit 1 \ +# First, clamp source mtime https://fedoraproject.org/wiki/Changes/ReproducibleBuildsClampMtimes \ +clamp_source_mtime "%1" "%2"; \ +# Get version without a dot (36 instead of 3.6), bash doesn't compare floats well \ +python_version=$(%1 -c "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))") \ +# compileall2 is an enhanced fork of stdlib compileall module for Python >= 3.4 \ +# and it was merged back to stdlib in Python >= 3.9 \ +# Only Python 3.7+ supports and needs the --invalidation-mode option \ +if [ "$python_version" -ge 39 ]; then \ +py39_byte_compile "%1" "%2"; \ +elif [ "$python_version" -ge 37 ]; then \ +py37_byte_compile "%1" "%2"; \ +elif [ "$python_version" -ge 34 ]; then \ +py34_byte_compile "%1" "%2"; \ +else \ +py2_byte_compile "%1" "%2"; \ +fi diff --git a/SOURCES/macros.python b/SOURCES/macros.python new file mode 100644 index 0000000..0ba4d78 --- /dev/null +++ b/SOURCES/macros.python @@ -0,0 +1,176 @@ +# Memoize a macro to avoid calling the same expensive code multiple times in +# the specfile. +# There is no error handling, +# memoizing an undefined macro (or using such a key) has undefined behavior. +# Options: +# -n - The name of the macro to wrap +# -k - The name of the macro to use as a cache key +%_python_memoize(n:k:) %{lua: +local name = opt.n +-- NB: We use rpm.expand() here instead of the macros table to make sure errors +-- are propogated properly. +local cache_key = rpm.expand("%{" .. opt.k .. "}") +if not _python_macro_cache then + -- This is intentionally a global lua table + _python_macro_cache = {} +end +if not _python_macro_cache[cache_key] then + _python_macro_cache[cache_key] = {} +end +if not _python_macro_cache[cache_key][name] then + _python_macro_cache[cache_key][name] = rpm.expand("%{" .. name .. "}") +end +print(_python_macro_cache[cache_key][name]) +} + +# unversioned macros: used with user defined __python, no longer part of rpm >= 4.15 +# __python is defined to error by default in the srpm macros +# nb: $RPM_BUILD_ROOT is not set when the macros are expanded (at spec parse time) +# so we set it manually (to empty string), making our Python prefer the correct install scheme location +# platbase/base is explicitly set to %%{_prefix} to support custom values, such as /app for flatpaks +%__python_sitelib %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_path('purelib', vars={'platbase': '%{_prefix}', 'base': '%{_prefix}'}))") +%python_sitelib %{_python_memoize -n __python_sitelib -k __python} + +%__python_sitearch %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_path('platlib', vars={'platbase': '%{_prefix}', 'base': '%{_prefix}'}))") +%python_sitearch %{_python_memoize -n __python_sitearch -k __python} + +%__python_version %(RPM_BUILD_ROOT= %{__python} -Esc "import sys; sys.stdout.write('{0.major}.{0.minor}'.format(sys.version_info))") +%python_version %{_python_memoize -n __python_version -k __python} + +%__python_version_nodots %(RPM_BUILD_ROOT= %{__python} -Esc "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))") +%python_version_nodots %{_python_memoize -n __python_version_nodots -k __python} + +%__python_platform %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_platform())") +%python_platform %{_python_memoize -n __python_platform -k __python} + +%__python_platform_triplet %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_config_var('MULTIARCH'))") +%python_platform_triplet %{_python_memoize -n __python_platform_triplet -k __python} + +%__python_ext_suffix %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))") +%python_ext_suffix %{_python_memoize -n __python_ext_suffix -k __python} + +%__python_cache_tag %(RPM_BUILD_ROOT= %{__python} -Esc "import sys; print(sys.implementation.cache_tag)") +%python_cache_tag %{_python_memoize -n __python_cache_tag -k __python} + +%py_setup setup.py +%_py_shebang_s s +%__py_shebang_P %(RPM_BUILD_ROOT= %{__python} -Esc "import sys; print('P' if hasattr(sys.flags, 'safe_path') else '')") +%_py_shebang_P %{_python_memoize -n __py_shebang_P -k __python} +%py_shbang_opts -%{?_py_shebang_s}%{?_py_shebang_P} +%py_shbang_opts_nodash %(opts=%{py_shbang_opts}; echo ${opts#-}) +%py_shebang_flags %(opts=%{py_shbang_opts}; echo ${opts#-}) +%py_shebang_fix %{expand:\\\ + if [ -z "%{?py_shebang_flags}" ]; then + shebang_flags="-k" + else + shebang_flags="-ka%{py_shebang_flags}" + fi + %{__python} -B %{_rpmconfigdir}/redhat/pathfix.py -pni %{__python} $shebang_flags} + +# Use the slashes after expand so that the command starts on the same line as +# the macro +%py_build() %{expand:\\\ + CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\ + %{__python} %{py_setup} %{?py_setup_args} build --executable="%{__python} %{py_shbang_opts}" %{?*} +} + +%py_build_wheel() %{expand:\\\ + CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\ + %{__python} %{py_setup} %{?py_setup_args} bdist_wheel %{?*} +} + +%py_install() %{expand:\\\ + CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\ + %{__python} %{py_setup} %{?py_setup_args} install -O1 --skip-build --root %{buildroot} --prefix %{_prefix} %{?*} + rm -rfv %{buildroot}%{_bindir}/__pycache__ +} + +%py_install_wheel() %{expand:\\\ + %{__python} -m pip install -I dist/%{1} --root %{buildroot} --prefix %{_prefix} --no-deps --no-index --no-warn-script-location + rm -rfv %{buildroot}%{_bindir}/__pycache__ + for distinfo in %{buildroot}%{python_sitelib}/*.dist-info %{buildroot}%{python_sitearch}/*.dist-info; do + if [ -f ${distinfo}/direct_url.json ]; then + rm -fv ${distinfo}/direct_url.json + sed -i '/direct_url.json/d' ${distinfo}/RECORD + fi + done +} + +# With $PATH and $PYTHONPATH set to the %%buildroot, +# try to import the Python module(s) given as command-line args or read from file (-f). +# Respect the custom values of %%py_shebang_flags or set nothing if it's undefined. +# Filter and check import on only top-level modules using -t flag. +# Exclude unwanted modules by passing their globs to -e option. +# Useful as a smoke test in %%check when running tests is not feasible. +# Use spaces or commas as separators if providing list directly. +# Use newlines as separators if providing list in a file. +%py_check_import(e:tf:) %{expand:\\\ + PATH="%{buildroot}%{_bindir}:$PATH"\\\ + PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python_sitearch}:%{buildroot}%{python_sitelib}}"\\\ + _PYTHONSITE="%{buildroot}%{python_sitearch}:%{buildroot}%{python_sitelib}"\\\ + PYTHONDONTWRITEBYTECODE=1\\\ + %{lua: + local command = "%{__python} " + if rpm.expand("%{?py_shebang_flags}") ~= "" then + command = command .. "-%{py_shebang_flags}" + end + command = command .. " %{_rpmconfigdir}/redhat/import_all_modules.py " + -- handle multiline arguments correctly, see https://bugzilla.redhat.com/2018809 + local args=rpm.expand('%{?**}'):gsub("[%s\\\\]*%s+", " ") + print(command .. args) + } +} + +%python_provide() %{lua: + local python = require "fedora.srpm.python" + function string.starts(String,Start) + return string.sub(String,1,string.len(Start))==Start + end + local package = rpm.expand("%{?1}") + local vr = rpm.expand("%{?epoch:%{epoch}:}%{version}-%{release}") + local provides = python.python_altprovides(package, vr) + local default_python3_pkgversion = rpm.expand("%{__default_python3_pkgversion}") + if (string.starts(package, "python3-")) then + for i, provide in ipairs(provides) do + print("\\nProvides: " .. provide) + end + --Obsoleting the previous default python package (if it doesn't have isa) + if (string.sub(package, "-1") ~= ")") then + print("\\nObsoletes: python-") + print(string.sub(package,9,string.len(package))) + print(" < " .. vr) + end + elseif (string.starts(package, "python" .. default_python3_pkgversion .. "-")) then + for i, provide in ipairs(provides) do + print("\\nProvides: " .. provide) + end + --Obsoleting the previous default python package (if it doesn't have isa) + if (string.sub(package, "-1") ~= ")") then + print("\\nObsoletes: python-") + print(string.sub(package,8+string.len(default_python3_pkgversion),string.len(package))) + print(" < " .. vr) + end + elseif (string.starts(package, "python")) then + --No unversioned provides as other python3 cases are not the default + elseif (string.starts(package, "pypy")) then + --No unversioned provides as pypy is not default either + else + print("%python_provide: ERROR: ") + print(package) + print(" not recognized.") + end +} + +# Environment variables for testing used standalone, e.g.: +# %%{py_test_envvars} %%{python} -m unittest +%py_test_envvars %{expand:\\\ + CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\ + PATH="%{buildroot}%{_bindir}:$PATH"\\\ + PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python_sitearch}:%{buildroot}%{python_sitelib}}"\\\ + PYTHONDONTWRITEBYTECODE=1\\\ + %{?__pytest_addopts:PYTEST_ADDOPTS="${PYTEST_ADDOPTS:-} %{__pytest_addopts}"}\\\ + PYTEST_XDIST_AUTO_NUM_WORKERS="${PYTEST_XDIST_AUTO_NUM_WORKERS:-%{_smp_build_ncpus}}"} + +%python_disable_dependency_generator() \ +%undefine __pythondist_requires \ +%{nil} diff --git a/SOURCES/macros.python-srpm b/SOURCES/macros.python-srpm new file mode 100644 index 0000000..d10a422 --- /dev/null +++ b/SOURCES/macros.python-srpm @@ -0,0 +1,295 @@ +# There are multiple Python 3 versions packaged, but only one can be the "main" version +# That means that it owns the "python3" namespace: +# - python3 package name +# - /usr/bin/python3 command +# - python3-foo packages are meant for this version +# Other versions of Python 3 always contain the version in the namespace: +# - python3.XX package name +# - /usr/bin/python3.XX command +# - python3.XX-foo packages (if allowed) +# +# Python spec files use the version defined here to determine defaults for the +# %%py_provides and %%python_provide macros, as well as for the "pythonname" generator that +# provides python3-foo for python3.XX-foo and vice versa for the default "main" version. +# E.g. in Fedora 33, python3.9-foo will provide python3-foo, +# python3-foo will provide python3.9-foo. +# +# There are two macros: +# +# This always contains the major.minor version (with dots), default for %%python3_version. +%__default_python3_version 3.12 +# +# The pkgname version that determines the alternative provide name (e.g. python3.9-foo), +# set to the same as above, but historically hasn't included the dot. +# This is left intentionally a separate macro, in case the naming convention ever changes. +%__default_python3_pkgversion %__default_python3_version + +# python3_pkgversion specifies the version of Python 3 in the distro. +# For Fedora, this is usually just "3". +# It can be a specific version distro-wide (e.g. "36" in EPEL7). +# Alternatively, it can be overridden in spec (e.g. to "3.8") when building for alternate Python stacks. +%python3_pkgversion 3 + +# Define the Python interpreter paths in the SRPM macros so that +# - they can be used in Build/Requires +# - they can be used in non-Python packages where requiring pythonX-devel would +# be an overkill + +# use the underscored macros to redefine the behavior of %%python3_version etc. +%__python2 /usr/bin/python2 +%__python3 /usr/bin/python%{python3_pkgversion} + +# use the non-underscored macros to refer to Python in spec, etc. +%python2 %__python2 +%python3 %__python3 + +# See https://fedoraproject.org/wiki/Changes/PythonMacroError +%__python %{error:attempt to use unversioned python, define %%__python to %{__python2} or %{__python3} explicitly} + +# Users can use %%python only if they redefined %%__python (e.g. to %%__python3) +%python %__python + +# Define where Python wheels will be stored and the prefix of -wheel packages +# - In Fedora we want wheel subpackages named e.g. `python-pip-wheel` that +# install packages into `/usr/share/python-wheels`. Both names are not +# versioned, because they're used by all Python 3 stacks. +# - In RHEL we want wheel packages named e.g. `python3-pip-wheel` and +# `python3.11-pip-wheel` that install packages into similarly versioned +# locations. We want each Python stack in RHEL to have their own wheels, +# because the main python3 wheels (which we can't upgrade) will likely be +# quite old by the time we're adding new alternate Python stacks. +# - In ELN we want to follow Fedora, because builds for ELN and Fedora rawhide +# need to be interoperable. +%python_wheel_pkg_prefix python%{?rhel:%{!?eln:%{python3_pkgversion}}} +%python_wheel_dir %{_datadir}/%{python_wheel_pkg_prefix}-wheels + + +### BRP scripts (and related macros) + +## Automatically compile python files +%py_auto_byte_compile 1 +## Should python bytecompilation errors terminate a build? +%_python_bytecompile_errors_terminate_build 1 +## Should python bytecompilation compile outside python specific directories? +## This always causes errors when enabled, see https://fedoraproject.org/wiki/Changes/No_more_automagic_Python_bytecompilation_phase_3 +%_python_bytecompile_extra 0 +## Helper macro to unset $SOURCE_DATE_EPOCH if %%clamp_mtime_to_source_date_epoch is not set +## https://fedoraproject.org/wiki/Changes/ReproducibleBuildsClampMtimes#Python_bytecode +%__env_unset_source_date_epoch_if_not_clamp_mtime %[0%{?clamp_mtime_to_source_date_epoch} == 0 ? "env -u SOURCE_DATE_EPOCH" : "env"] + +## The individual BRP scripts +%__brp_python_bytecompile %{__env_unset_source_date_epoch_if_not_clamp_mtime} %{_rpmconfigdir}/redhat/brp-python-bytecompile "" "%{?_python_bytecompile_errors_terminate_build}" "%{?_python_bytecompile_extra}" "%{?_smp_build_ncpus:-j%{_smp_build_ncpus}}" +%__brp_fix_pyc_reproducibility %{_rpmconfigdir}/redhat/brp-fix-pyc-reproducibility +%__brp_python_hardlink %{_rpmconfigdir}/redhat/brp-python-hardlink + +## This macro is included in redhat-rpm-config's %%__os_install_post +# Note that the order matters: +# 1. brp-python-bytecompile can create (or replace) pyc files +# 2. brp-fix-pyc-reproducibility can modify the pyc files from above +# 3. brp-python-hardlink de-duplicates identical pyc files +%__os_install_post_python \ + %{?py_auto_byte_compile:%{?__brp_python_bytecompile}} \ + %{?py_reproducible_pyc_path:%{?__brp_fix_pyc_reproducibility} "%{py_reproducible_pyc_path}"} \ + %{?__brp_python_hardlink} \ +%{nil} + + +# === Macros for Build/Requires tags using Python dist tags === +# - https://fedoraproject.org/wiki/Changes/Automatic_Provides_for_Python_RPM_Packages +# - These macros need to be in macros.python-srpm, because BuildRequires tags +# get rendered as runtime requires into the metadata of SRPMs. + +# Converts Python dist name to a canonical format +%py_dist_name() %{lua:\ + name = rpm.expand("%{?1:%{1}}");\ + canonical = string.gsub(string.lower(name), "[^%w%[%]]+", "-");\ + print(canonical);\ +} + +# Creates Python 2 dist tag(s) after converting names to canonical format +# Needs to first put all arguments into a list, because invoking a different +# macro (%%py_dist_name) overwrites them +%py2_dist() %{lua:\ + args = {}\ + arg = 1\ + while (true) do\ + name = rpm.expand("%{?" .. arg .. ":%{" .. arg .. "}}");\ + if (name == nil or name == '') then\ + break\ + end\ + args[arg] = name\ + arg = arg + 1\ + end\ + for arg, name in ipairs(args) do\ + canonical = rpm.expand("%py_dist_name " .. name);\ + print("python2dist(" .. canonical .. ") ");\ + end\ +} + +# Creates Python 3 dist tag(s) after converting names to canonical format +# Needs to first put all arguments into a list, because invoking a different +# macro (%%py_dist_name) overwrites them +%py3_dist() %{lua:\ + python3_pkgversion = rpm.expand("%python3_pkgversion");\ + args = {}\ + arg = 1\ + while (true) do\ + name = rpm.expand("%{?" .. arg .. ":%{" .. arg .. "}}");\ + if (name == nil or name == '') then\ + break\ + end\ + args[arg] = name\ + arg = arg + 1\ + end\ + for arg, name in ipairs(args) do\ + canonical = rpm.expand("%py_dist_name " .. name);\ + print("python" .. python3_pkgversion .. "dist(" .. canonical .. ") ");\ + end\ +} + +# Macro to replace overly complicated references to PyPI source files. +# Expands to the pythonhosted URL for a package +# Accepts zero to three arguments: +# 1: The PyPI project name, defaulting to %%srcname if it is defined, then +# %%pypi_name if it is defined, then just %%name. +# 2: The PYPI version, defaulting to %%version with tildes stripped. +# 3: The file extension, defaulting to "tar.gz". (A period will be added +# automatically.) +# Requires %%__pypi_url and %%__pypi_default_extension to be defined. +%__pypi_url https://files.pythonhosted.org/packages/source/ +%__pypi_default_extension tar.gz + +%pypi_source() %{lua: + local src = rpm.expand('%1') + local ver = rpm.expand('%2') + local ext = rpm.expand('%3') + local url = rpm.expand('%__pypi_url') +\ + -- If no first argument, try %srcname, then %pypi_name, then %name + -- Note that rpm leaves macros unchanged if they are not defined. + if src == '%1' then + src = rpm.expand('%srcname') + end + if src == '%srcname' then + src = rpm.expand('%pypi_name') + end + if src == '%pypi_name' then + src = rpm.expand('%name') + end +\ + -- If no second argument, use %version + if ver == '%2' then + ver = rpm.expand('%version'):gsub('~', '') + end +\ + -- If no third argument, use the preset default extension + if ext == '%3' then + ext = rpm.expand('%__pypi_default_extension') + end +\ + local first = string.sub(src, 1, 1) +\ + print(url .. first .. '/' .. src .. '/' .. src .. '-' .. ver .. '.' .. ext) +} + +%py_provides() %{lua: + local python = require 'fedora.srpm.python' + local rhel = rpm.expand('%{?rhel}') + local name = rpm.expand('%1') + if name == '%1' then + rpm.expand('%{error:%%py_provides requires at least 1 argument, the name to provide}') + end + local evr = rpm.expand('%2') + if evr == '%2' then + evr = rpm.expand('%{?epoch:%{epoch}:}%{version}-%{release}') + end + print('Provides: ' .. name .. ' = ' .. evr .. '\\n') + local provides = python.python_altprovides(name, evr) + for i, provide in ipairs(provides) do + print('Provides: ' .. provide .. '\\n') + end + -- We only generate these Obsoletes on CentOS/RHEL to provide clean upgrade + -- path, e.g. python3-foo obsoletes python3.9-foo from previous RHEL. + -- In Fedora this is not needed as we don't ship ecosystem packages + -- for alternative Python interpreters. + if rhel ~= '' then + -- Create Obsoletes only if the name does not end in a parenthesis, + -- as Obsoletes can't include parentheses. + -- This most commonly happens when the name contains an isa. + if (string.sub(name, "-1") ~= ")") then + local obsoletes = python.python_altobsoletes(name, evr) + for i, obsolete in ipairs(obsoletes) do + print('Obsoletes: ' .. obsolete .. '\\n') + end + end + end +} + +%python_extras_subpkg(n:i:f:FaA) %{expand:%{lua: + local option_n = '-n (name of the base package)' + local option_i = '-i (buildroot path to metadata)' + local option_f = '-f (builddir path to a filelist)' + local option_F = '-F (skip %%files section)' + local option_a = '-a (insert BuildArch: noarch)' + local option_A = '-A (do not insert BuildArch: noarch (default))' + local value_n = rpm.expand('%{-n*}') + local value_i = rpm.expand('%{-i*}') + local value_f = rpm.expand('%{-f*}') + local value_F = rpm.expand('%{-F}') + local value_a = rpm.expand('%{-a}') + local value_A = rpm.expand('%{-A}') + local args = rpm.expand('%{*}') + if value_n == '' then + rpm.expand('%{error:%%%0: missing option ' .. option_n .. '}') + end + if value_i == '' and value_f == '' and value_F == '' then + rpm.expand('%{error:%%%0: missing option ' .. option_i .. ' or ' .. option_f .. ' or ' .. option_F .. '}') + end + if value_i ~= '' and value_f ~= '' then + rpm.expand('%{error:%%%0: simultaneous ' .. option_i .. ' and ' .. option_f .. ' options are not possible}') + end + if value_i ~= '' and value_F ~= '' then + rpm.expand('%{error:%%%0: simultaneous ' .. option_i .. ' and ' .. option_F .. ' options are not possible}') + end + if value_f ~= '' and value_F ~= '' then + rpm.expand('%{error:%%%0: simultaneous ' .. option_f .. ' and ' .. option_F .. ' options are not possible}') + end + if value_a ~= '' and value_A ~= '' then + rpm.expand('%{error:%%%0: simultaneous ' .. option_a .. ' and ' .. option_A .. ' options are not possible}') + end + if args == '' then + rpm.expand('%{error:%%%0 requires at least one argument with "extras" name}') + end + local requires = 'Requires: ' .. value_n .. ' = %{?epoch:%{epoch}:}%{version}-%{release}' + for extras in args:gmatch('[^%s,]+') do + local rpmname = value_n .. '+' .. extras + local pkgdef = '%package -n ' .. rpmname + local summary = 'Summary: Metapackage for ' .. value_n .. ': ' .. extras .. ' extras' + local description = '%description -n ' .. rpmname .. '\\\n' + local current_line = 'This is a metapackage bringing in' + for _, word in ipairs({extras, 'extras', 'requires', 'for', value_n .. '.'}) do + local line = current_line .. ' ' .. word + if line:len() > 79 then + description = description .. current_line .. '\\\n' + current_line = word + else + current_line = line + end + end + description = description .. current_line .. '\\\n' .. + 'It makes sure the dependencies are installed.\\\n' + local files = '' + if value_i ~= '' then + files = '%files -n ' .. rpmname .. '\\\n' .. '%ghost ' .. value_i + elseif value_f ~= '' then + files = '%files -n ' .. rpmname .. ' -f ' .. value_f + end + local tags = summary .. '\\\n' .. requires + if value_a ~= '' then + tags = tags .. '\\\nBuildArch: noarch' + end + for i, line in ipairs({pkgdef, tags, description, files, ''}) do + print(line .. '\\\n') + end + end +}} diff --git a/SOURCES/macros.python3 b/SOURCES/macros.python3 new file mode 100644 index 0000000..7197433 --- /dev/null +++ b/SOURCES/macros.python3 @@ -0,0 +1,126 @@ +# nb: $RPM_BUILD_ROOT is not set when the macros are expanded (at spec parse time) +# so we set it manually (to empty string), making our Python prefer the correct install scheme location +# platbase/base is explicitly set to %%{_prefix} to support custom values, such as /app for flatpaks +%__python3_sitelib %(RPM_BUILD_ROOT= %{__python3} -Esc "import sysconfig; print(sysconfig.get_path('purelib', vars={'platbase': '%{_prefix}', 'base': '%{_prefix}'}))") +%python3_sitelib %{_python_memoize -n __python3_sitelib -k __python3} + +%__python3_sitearch %(RPM_BUILD_ROOT= %{__python3} -Esc "import sysconfig; print(sysconfig.get_path('platlib', vars={'platbase': '%{_prefix}', 'base': '%{_prefix}'}))") +%python3_sitearch %{_python_memoize -n __python3_sitearch -k __python3} + +%__python3_version %(RPM_BUILD_ROOT= %{__python3} -Esc "import sys; sys.stdout.write('{0.major}.{0.minor}'.format(sys.version_info))") +%python3_version %{_python_memoize -n __python3_version -k __python3} + +%__python3_version_nodots %(RPM_BUILD_ROOT= %{__python3} -Esc "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))") +%python3_version_nodots %{_python_memoize -n __python3_version_nodots -k __python3} + +%__python3_platform %(RPM_BUILD_ROOT= %{__python3} -Esc "import sysconfig; print(sysconfig.get_platform())") +%python3_platform %{_python_memoize -n __python3_platform -k __python3} + +%__python3_platform_triplet %(RPM_BUILD_ROOT= %{__python3} -Esc "import sysconfig; print(sysconfig.get_config_var('MULTIARCH'))") +%python3_platform_triplet %{_python_memoize -n __python3_platform_triplet -k __python3} + +%__python3_ext_suffix %(RPM_BUILD_ROOT= %{__python3} -Esc "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))") +%python3_ext_suffix %{_python_memoize -n __python3_ext_suffix -k __python3} + +%__python3_cache_tag %(RPM_BUILD_ROOT= %{__python3} -Esc "import sys; print(sys.implementation.cache_tag)") +%python3_cache_tag %{_python_memoize -n __python3_cache_tag -k __python3} + +%py3dir %{_builddir}/python3-%{name}-%{version}-%{release} + +%_py3_shebang_s s +%__py3_shebang_P %(RPM_BUILD_ROOT= %{__python3} -Ic "import sys; print('P' if hasattr(sys.flags, 'safe_path') else '')") +%_py3_shebang_P %{_python_memoize -n __py3_shebang_P -k __python3} +%py3_shbang_opts -%{?_py3_shebang_s}%{?_py3_shebang_P} +%py3_shbang_opts_nodash %(opts=%{py3_shbang_opts}; echo ${opts#-}) +%py3_shebang_flags %(opts=%{py3_shbang_opts}; echo ${opts#-}) +%py3_shebang_fix %{expand:\\\ + if [ -z "%{?py3_shebang_flags}" ]; then + shebang_flags="-k" + else + shebang_flags="-ka%{py3_shebang_flags}" + fi + %{__python3} -B %{_rpmconfigdir}/redhat/pathfix.py -pni %{__python3} $shebang_flags} + +# Use the slashes after expand so that the command starts on the same line as +# the macro +%py3_build() %{expand:\\\ + CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\ + %{__python3} %{py_setup} %{?py_setup_args} build --executable="%{__python3} %{py3_shbang_opts}" %{?*} +} + +%py3_build_wheel() %{expand:\\\ + CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\ + %{__python3} %{py_setup} %{?py_setup_args} bdist_wheel %{?*} +} + +%py3_install() %{expand:\\\ + CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\ + %{__python3} %{py_setup} %{?py_setup_args} install -O1 --skip-build --root %{buildroot} --prefix %{_prefix} %{?*} + rm -rfv %{buildroot}%{_bindir}/__pycache__ +} + +%py3_install_wheel() %{expand:\\\ + %{__python3} -m pip install -I dist/%{1} --root %{buildroot} --prefix %{_prefix} --no-deps --no-index --no-warn-script-location + rm -rfv %{buildroot}%{_bindir}/__pycache__ + for distinfo in %{buildroot}%{python3_sitelib}/*.dist-info %{buildroot}%{python3_sitearch}/*.dist-info; do + if [ -f ${distinfo}/direct_url.json ]; then + rm -fv ${distinfo}/direct_url.json + sed -i '/direct_url.json/d' ${distinfo}/RECORD + fi + done +} + +# With $PATH and $PYTHONPATH set to the %%buildroot, +# try to import the Python 3 module(s) given as command-line args or read from file (-f). +# Respect the custom values of %%py3_shebang_flags or set nothing if it's undefined. +# Filter and check import on only top-level modules using -t flag. +# Exclude unwanted modules by passing their globs to -e option. +# Useful as a smoke test in %%check when running tests is not feasible. +# Use spaces or commas as separators if providing list directly. +# Use newlines as separators if providing list in a file. +%py3_check_import(e:tf:) %{expand:\\\ + PATH="%{buildroot}%{_bindir}:$PATH"\\\ + PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}}"\\\ + _PYTHONSITE="%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}"\\\ + PYTHONDONTWRITEBYTECODE=1\\\ + %{lua: + local command = "%{__python3} " + if rpm.expand("%{?py3_shebang_flags}") ~= "" then + command = command .. "-%{py3_shebang_flags}" + end + command = command .. " %{_rpmconfigdir}/redhat/import_all_modules.py " + -- handle multiline arguments correctly, see https://bugzilla.redhat.com/2018809 + local args=rpm.expand('%{?**}'):gsub("[%s\\\\]*%s+", " ") + print(command .. args) + } +} + +# This only supports Python 3.5+ and will never work with Python 2. +# Hence, it has no Python version in the name. +%pycached() %{lua: + path = rpm.expand("%{?*}") + if (string.sub(path, "-3") ~= ".py") then + rpm.expand("%{error:%%pycached can only be used with paths explicitly ending with .py}") + else + print(path) + pyminor = path:match("/python3.(%d+)/") or "*" + dirname = path:match("(.*/)") + modulename = path:match(".*/([^/]+).py") + -- %%python3_cache_tag is not used here because this macro supports not-installed CPythons + print("\\n" .. dirname .. "__pycache__/" .. modulename .. ".cpython-3" .. pyminor .. "{,.opt-?}.pyc") + end +} + +# Environment variables used by %%pytest, %%tox or standalone, e.g.: +# %%{py3_test_envvars} %%{python3} -m unittest +%py3_test_envvars %{expand:\\\ + CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\ + PATH="%{buildroot}%{_bindir}:$PATH"\\\ + PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}}"\\\ + PYTHONDONTWRITEBYTECODE=1\\\ + %{?__pytest_addopts:PYTEST_ADDOPTS="${PYTEST_ADDOPTS:-} %{__pytest_addopts}"}\\\ + PYTEST_XDIST_AUTO_NUM_WORKERS="${PYTEST_XDIST_AUTO_NUM_WORKERS:-%{_smp_build_ncpus}}"} + +# This is intended for Python 3 only, hence also no Python version in the name. +%__pytest /usr/bin/pytest%(test %{python3_pkgversion} == 3 || echo -%{python3_version}) +%pytest %py3_test_envvars %__pytest diff --git a/SOURCES/pathfix.py b/SOURCES/pathfix.py new file mode 100644 index 0000000..1d7db3a --- /dev/null +++ b/SOURCES/pathfix.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 + +import sys +import os +from stat import * +import getopt + +err = sys.stderr.write +dbg = err +rep = sys.stdout.write + +new_interpreter = None +preserve_timestamps = False +create_backup = True +keep_flags = False +add_flags = b'' + + +def main(): + global new_interpreter + global preserve_timestamps + global create_backup + global keep_flags + global add_flags + + usage = ('usage: %s -i /interpreter -p -n -k -a file-or-directory ...\n' % + sys.argv[0]) + try: + opts, args = getopt.getopt(sys.argv[1:], 'i:a:kpn') + except getopt.error as msg: + err(str(msg) + '\n') + err(usage) + sys.exit(2) + for o, a in opts: + if o == '-i': + new_interpreter = a.encode() + if o == '-p': + preserve_timestamps = True + if o == '-n': + create_backup = False + if o == '-k': + keep_flags = True + if o == '-a': + add_flags = a.encode() + if b' ' in add_flags: + err("-a option doesn't support whitespaces") + sys.exit(2) + if not new_interpreter or not new_interpreter.startswith(b'/') or \ + not args: + err('-i option or file-or-directory missing\n') + err(usage) + sys.exit(2) + bad = 0 + for arg in args: + if os.path.isdir(arg): + if recursedown(arg): bad = 1 + elif os.path.islink(arg): + err(arg + ': will not process symbolic links\n') + bad = 1 + else: + if fix(arg): bad = 1 + sys.exit(bad) + + +def ispython(name): + return name.endswith('.py') + + +def recursedown(dirname): + dbg('recursedown(%r)\n' % (dirname,)) + bad = 0 + try: + names = os.listdir(dirname) + except OSError as msg: + err('%s: cannot list directory: %r\n' % (dirname, msg)) + return 1 + names.sort() + subdirs = [] + for name in names: + if name in (os.curdir, os.pardir): continue + fullname = os.path.join(dirname, name) + if os.path.islink(fullname): pass + elif os.path.isdir(fullname): + subdirs.append(fullname) + elif ispython(name): + if fix(fullname): bad = 1 + for fullname in subdirs: + if recursedown(fullname): bad = 1 + return bad + + +def fix(filename): +## dbg('fix(%r)\n' % (filename,)) + try: + f = open(filename, 'rb') + except IOError as msg: + err('%s: cannot open: %r\n' % (filename, msg)) + return 1 + with f: + line = f.readline() + fixed = fixline(line) + if line == fixed: + rep(filename+': no change\n') + return + head, tail = os.path.split(filename) + tempname = os.path.join(head, '@' + tail) + try: + g = open(tempname, 'wb') + except IOError as msg: + err('%s: cannot create: %r\n' % (tempname, msg)) + return 1 + with g: + rep(filename + ': updating\n') + g.write(fixed) + BUFSIZE = 8*1024 + while 1: + buf = f.read(BUFSIZE) + if not buf: break + g.write(buf) + + # Finishing touch -- move files + + mtime = None + atime = None + # First copy the file's mode to the temp file + try: + statbuf = os.stat(filename) + mtime = statbuf.st_mtime + atime = statbuf.st_atime + os.chmod(tempname, statbuf[ST_MODE] & 0o7777) + except OSError as msg: + err('%s: warning: chmod failed (%r)\n' % (tempname, msg)) + # Then make a backup of the original file as filename~ + if create_backup: + try: + os.rename(filename, filename + '~') + except OSError as msg: + err('%s: warning: backup failed (%r)\n' % (filename, msg)) + else: + try: + os.remove(filename) + except OSError as msg: + err('%s: warning: removing failed (%r)\n' % (filename, msg)) + # Now move the temp file to the original file + try: + os.rename(tempname, filename) + except OSError as msg: + err('%s: rename failed (%r)\n' % (filename, msg)) + return 1 + if preserve_timestamps: + if atime and mtime: + try: + os.utime(filename, (atime, mtime)) + except OSError as msg: + err('%s: reset of timestamp failed (%r)\n' % (filename, msg)) + return 1 + # Return success + return 0 + + +def parse_shebang(shebangline): + shebangline = shebangline.rstrip(b'\n') + start = shebangline.find(b' -') + if start == -1: + return b'' + return shebangline[start:] + + +def populate_flags(shebangline): + old_flags = b'' + if keep_flags: + old_flags = parse_shebang(shebangline) + if old_flags: + old_flags = old_flags[2:] + if not (old_flags or add_flags): + return b'' + # On Linux, the entire string following the interpreter name + # is passed as a single argument to the interpreter. + # e.g. "#! /usr/bin/python3 -W Error -s" runs "/usr/bin/python3 "-W Error -s" + # so shebang should have single '-' where flags are given and + # flag might need argument for that reasons adding new flags is + # between '-' and original flags + # e.g. #! /usr/bin/python3 -sW Error + return b' -' + add_flags + old_flags + + +def fixline(line): + if not line.startswith(b'#!'): + return line + + if b"python" not in line: + return line + + flags = populate_flags(line) + return b'#! ' + new_interpreter + flags + b'\n' + + +if __name__ == '__main__': + main() diff --git a/SOURCES/python.lua b/SOURCES/python.lua new file mode 100644 index 0000000..bd30a85 --- /dev/null +++ b/SOURCES/python.lua @@ -0,0 +1,101 @@ +-- Convenience Lua functions that can be used within Python srpm/rpm macros + +-- Determine alternate names provided from the given name. +-- Used in pythonname provides generator, python_provide and py_provides. +-- If only_3_to_3_X is false/nil/unused there are 2 rules: +-- python3-foo -> python-foo, python3.X-foo +-- python3.X-foo -> python-foo, python3-foo +-- If only_3_to_3_X is true there is only 1 rule: +-- python3-foo -> python3.X-foo +-- There is no python-foo -> rule, python-foo packages are version agnostic. +-- Returns a table/array with strings. Empty when no rule matched. +local function python_altnames(name, only_3_to_3_X) + local xy = rpm.expand('%{__default_python3_pkgversion}') + local altnames = {} + local replaced + -- NB: dash needs to be escaped! + if name:match('^python3%-') then + local prefixes = only_3_to_3_X and {} or {'python-'} + for i, prefix in ipairs({'python' .. xy .. '-', table.unpack(prefixes)}) do + replaced = name:gsub('^python3%-', prefix) + table.insert(altnames, replaced) + end + elseif name:match('^python' .. xy .. '%-') and not only_3_to_3_X then + for i, prefix in ipairs({'python-', 'python3-'}) do + replaced = name:gsub('^python' .. xy .. '%-', prefix) + table.insert(altnames, replaced) + end + end + return altnames +end + + +local function __python_alttags(name, evr, tag_type) + -- for the "provides" tag_type we want also unversioned provides + local only_3_to_3_X = tag_type ~= "provides" + local operator = tag_type == "provides" and ' = ' or ' < ' + + -- global cache that tells what package NEVRs were already processed for the + -- given tag type + if __python_alttags_beenthere == nil then + __python_alttags_beenthere = {} + end + if __python_alttags_beenthere[tag_type] == nil then + __python_alttags_beenthere[tag_type] = {} + end + __python_alttags_beenthere[tag_type][name .. ' ' .. evr] = true + local alttags = {} + for i, altname in ipairs(python_altnames(name, only_3_to_3_X)) do + table.insert(alttags, altname .. operator .. evr) + end + return alttags +end + +-- For any given name and epoch-version-release, return provides except self. +-- Uses python_altnames under the hood +-- Returns a table/array with strings. +local function python_altprovides(name, evr) + return __python_alttags(name, evr, "provides") +end + +-- For any given name and epoch-version-release, return versioned obsoletes except self. +-- Uses python_altnames under the hood +-- Returns a table/array with strings. +local function python_altobsoletes(name, evr) + return __python_alttags(name, evr, "obsoletes") +end + + +local function __python_alttags_once(name, evr, tag_type) + -- global cache that tells what provides were already processed + if __python_alttags_beenthere == nil + or __python_alttags_beenthere[tag_type] == nil + or __python_alttags_beenthere[tag_type][name .. ' ' .. evr] == nil then + return __python_alttags(name, evr, tag_type) + else + return nil + end +end + +-- Like python_altprovides but only return something once. +-- For each argument can only be used once, returns nil otherwise. +-- Previous usage of python_altprovides counts as well. +local function python_altprovides_once(name, evr) + return __python_alttags_once(name, evr, "provides") +end + +-- Like python_altobsoletes but only return something once. +-- For each argument can only be used once, returns nil otherwise. +-- Previous usage of python_altobsoletes counts as well. +local function python_altobsoletes_once(name, evr) + return __python_alttags_once(name, evr, "obsoletes") +end + + +return { + python_altnames = python_altnames, + python_altprovides = python_altprovides, + python_altobsoletes = python_altobsoletes, + python_altprovides_once = python_altprovides_once, + python_altobsoletes_once = python_altobsoletes_once, +} diff --git a/SPECS/python-rpm-macros.spec b/SPECS/python-rpm-macros.spec new file mode 100644 index 0000000..2d51664 --- /dev/null +++ b/SPECS/python-rpm-macros.spec @@ -0,0 +1,588 @@ +Name: python-rpm-macros +Summary: The common Python RPM macros + +URL: https://src.fedoraproject.org/rpms/python-rpm-macros/ + +# Macros: +Source101: macros.python +Source102: macros.python-srpm +Source104: macros.python3 +Source105: macros.pybytecompile + +# Lua files +Source201: python.lua + +# Python code +%global compileall2_version 0.7.1 +Source301: https://github.com/fedora-python/compileall2/raw/v%{compileall2_version}/compileall2.py +Source302: import_all_modules.py +%global pathfix_version 1.0.0 +Source303: https://github.com/fedora-python/pathfix/raw/v%{pathfix_version}/pathfix.py +Source304: clamp_source_mtime.py + +# BRP scripts +# This one is from redhat-rpm-config < 190 +# A new upstream is forming in https://github.com/rpm-software-management/python-rpm-packaging/blob/main/scripts/brp-python-bytecompile +# But our version is riddled with Fedora-isms +# We might eventually move to upstream source + Fedora patches, but we are not there yet +Source401: brp-python-bytecompile +# This one is from https://github.com/rpm-software-management/python-rpm-packaging/blob/main/scripts/brp-python-hardlink +# But we don't use a link in case it changes in upstream, there are no "versions" there yet +# This was removed from RPM 4.17+ so we maintain it here instead +Source402: brp-python-hardlink +# This one is from redhat-rpm-config < 190 +# It has no upstream yet +Source403: brp-fix-pyc-reproducibility + +# macros and lua: MIT +# import_all_modules.py: MIT +# compileall2.py, clamp_source_mtime.py: PSF-2.0 +# pathfix.py: PSF-2.0 +# brp scripts: GPL-2.0-or-later +License: MIT AND PSF-2.0 AND GPL-2.0-or-later + +# The package version MUST be always the same as %%{__default_python3_version}. +# To have only one source of truth, we load the macro and use it. +# The macro is defined in python-srpm-macros. +%{lua: +if posix.stat(rpm.expand('%{SOURCE102}')) then + rpm.load(rpm.expand('%{SOURCE102}')) +elseif posix.stat('macros.python-srpm') then + -- something is parsing the spec without _sourcedir macro properly set + rpm.load('macros.python-srpm') +end +} +Version: %{__default_python3_version} +Release: 8.1%{?dist} + +BuildArch: noarch + +# For %%__default_python3_pkgversion used in %%python_provide +# For python.lua +# For compileall2.py +Requires: python-srpm-macros = %{version}-%{release} + +# The packages are called python(3)-(s)rpm-macros +# We never want python3-rpm-macros to provide python-rpm-macros +# We opt out from all Python name-based automatic provides and obsoletes +%undefine __pythonname_provides +%undefine __pythonname_obsoletes + +%description +This package contains the unversioned Python RPM macros, that most +implementations should rely on. + +You should not need to install this package manually as the various +python?-devel packages require it. So install a python-devel package instead. + + +%package -n python-srpm-macros +Summary: RPM macros for building Python source packages + +# For directory structure and flags macros +# Versions before 190 contained some brp scripts moved into python-srpm-macros +Requires: redhat-rpm-config >= 190 + +# We bundle our own software here :/ +Provides: bundled(python3dist(compileall2)) = %{compileall2_version} + +%description -n python-srpm-macros +RPM macros for building Python source packages. + + +%package -n python3-rpm-macros +Summary: RPM macros for building Python 3 packages + +# For %%__python3 and %%python3 +Requires: python-srpm-macros = %{version}-%{release} + +# For %%py_setup and import_all_modules.py +Requires: python-rpm-macros = %{version}-%{release} + +%description -n python3-rpm-macros +RPM macros for building Python 3 packages. + + +%prep +%autosetup -c -T +cp -a %{sources} . + +# We want to have shebang in the script upstream but not here so +# the package with macros does not depend on Python. +sed -i '1s=^#!/usr/bin/env python3==' pathfix.py + + +%install +mkdir -p %{buildroot}%{rpmmacrodir} +install -m 644 macros.* %{buildroot}%{rpmmacrodir}/ + +mkdir -p %{buildroot}%{_rpmluadir}/fedora/srpm +install -p -m 644 -t %{buildroot}%{_rpmluadir}/fedora/srpm python.lua + +mkdir -p %{buildroot}%{_rpmconfigdir}/redhat +install -m 644 compileall2.py %{buildroot}%{_rpmconfigdir}/redhat/ +install -m 644 clamp_source_mtime.py %{buildroot}%{_rpmconfigdir}/redhat/ +install -m 644 import_all_modules.py %{buildroot}%{_rpmconfigdir}/redhat/ +install -m 644 pathfix.py %{buildroot}%{_rpmconfigdir}/redhat/ +install -m 755 brp-* %{buildroot}%{_rpmconfigdir}/redhat/ + + +# We define our own BRPs here to use the ones from the %%{buildroot}, +# that way, this package can be built when it includes them for the first time. +# It also ensures that: +# - our BRPs can execute +# - if our BRPs affect this package, we don't need to build it twice +%define add_buildroot() %{lua:print((macros[macros[1]]:gsub(macros._rpmconfigdir, macros.buildroot .. macros._rpmconfigdir)))} +%global __brp_python_bytecompile %{add_buildroot __brp_python_bytecompile} +%global __brp_python_hardlink %{add_buildroot __brp_python_hardlink} +%global __brp_fix_pyc_reproducibility %{add_buildroot __brp_fix_pyc_reproducibility} + + +%check +# no macros in comments +grep -E '^#[^%%]*%%[^%%]' %{buildroot}%{rpmmacrodir}/macros.* && exit 1 || true + + +%files +%{rpmmacrodir}/macros.python +%{rpmmacrodir}/macros.pybytecompile +%{_rpmconfigdir}/redhat/import_all_modules.py +%{_rpmconfigdir}/redhat/pathfix.py + +%files -n python-srpm-macros +%{rpmmacrodir}/macros.python-srpm +%{_rpmconfigdir}/redhat/compileall2.py +%{_rpmconfigdir}/redhat/clamp_source_mtime.py +%{_rpmconfigdir}/redhat/brp-python-bytecompile +%{_rpmconfigdir}/redhat/brp-python-hardlink +%{_rpmconfigdir}/redhat/brp-fix-pyc-reproducibility +%{_rpmluadir}/fedora/srpm/python.lua + +%files -n python3-rpm-macros +%{rpmmacrodir}/macros.python3 + + +%changelog +* Fri Oct 25 2024 MSVSphere Packaging Team - 3.12-8.1 +- Rebuilt for MSVSphere 10 + +* Tue Jun 25 2024 Cristian Le - 3.12-8.1 +- %%python_extras_subpkg: Add option -a to include BuildArch: noarch + +* Mon Jun 24 2024 Troy Dawson - 3.12-8 +- Bump release for June 2024 mass rebuild + +* Thu Jan 25 2024 Miro Hrončok - 3.12-7 +- %%py3_test_envvars: Only set $PYTEST_XDIST_AUTO_NUM_WORKERS if not already set + +* Mon Jan 22 2024 Fedora Release Engineering - 3.12-6 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild + +* Mon Oct 09 2023 Maxwell G - 3.12-5 +- Fix python macro memoizing to account for changing %%__python3 + +* Tue Sep 05 2023 Maxwell G - 3.12-4 +- Remove %%py3_build_egg and %%py3_install_egg macros. + +* Wed Aug 09 2023 Karolina Surma - 3.12-3 +- Declare the license as an SPDX expression + +* Fri Jul 21 2023 Fedora Release Engineering - 3.12-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_39_Mass_Rebuild + +* Tue Jun 13 2023 Tomáš Hrnčiar - 3.12-1 +- Update main Python to Python 3.12 +- https://fedoraproject.org/wiki/Changes/Python3.12 + +* Thu Mar 16 2023 Miro Hrončok - 3.11-10 +- Don't assume %%_smp_mflags only ever contains -jX, use -j%%_smp_build_ncpus directly +- Fixes: rhbz#2179149 + +* Fri Jan 20 2023 Miro Hrončok - 3.11-9 +- Memoize values of macros that execute python to get their value +- Fixes: rhbz#2155505 + +* Fri Jan 20 2023 Fedora Release Engineering - 3.11-8 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_38_Mass_Rebuild + +* Mon Dec 19 2022 Miro Hrončok - 3.11-7 +- Bytecompilation: Unset $SOURCE_DATE_EPOCH when %%clamp_mtime_to_source_date_epoch is not set +- Bytecompilation: Pass --invalidation-mode=timestamp to compileall (on Python 3.7+) +- Bytecompilation: Clamp source mtime: https://fedoraproject.org/wiki/Changes/ReproducibleBuildsClampMtimes +- Bytecompilation: Compile Python files in parallel, according to %%_smp_mflags + +* Sun Nov 13 2022 Miro Hrončok - 3.11-6 +- Set PYTEST_XDIST_AUTO_NUM_WORKERS=%%{_smp_build_ncpus} from %%pytest +- pytest-xdist 3+ respects this value when -n auto is used +- Expose the environment variables used by %%pytest via %%{py3_test_envvars} + +* Tue Oct 25 2022 Lumír Balhar - 3.11-5 +- Include pathfix.py in this package + +* Fri Jul 22 2022 Fedora Release Engineering - 3.10-4 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_37_Mass_Rebuild + +* Tue Jul 19 2022 Miro Hrončok - 3.11-3 +- Add "P" to %%py3_shbang_opts, %%py3_shbang_opts_nodash, %%py3_shebang_flags + and to %%py_shbang_opts, %%py_shbang_opts_nodash, %%py_shebang_flags +- https://fedoraproject.org/wiki/Changes/PythonSafePath + +* Mon Jun 20 2022 Miro Hrončok - 3.11-2 +- Define %%python3_cache_tag / %%python_cache_tag, e.g. cpython-311 + +* Mon Jun 13 2022 Tomáš Hrnčiar - 3.11-1 +- Update main Python to Python 3.11 +- https://fedoraproject.org/wiki/Changes/Python3.11 + +* Thu May 26 2022 Owen Taylor - 3.10-18 +- Support installing to %%{_prefix} other than /usr + +* Tue Feb 08 2022 Tomas Orsava - 3.10-17 +- %%py_provides: Do not generate Obsoletes for names containing parentheses + +* Mon Jan 31 2022 Miro Hrončok - 3.10-16 +- Explicitly opt-out from Python name-based provides and obsoletes generators + +* Tue Dec 21 2021 Tomas Orsava - 3.10-15 +- Add lua helper functions to make it possible to automatically generate + Obsoletes tags +- Modify the %%py_provides macro to also generate Obsoletes tags on CentOS/RHEL + +* Wed Dec 08 2021 Miro Hrončok - 3.10-14 +- Set %%__python3 value according to %%python3_pkgversion + I.e. when %%python3_pkgversion is 3.12, %%__python3 is /usr/bin/python3.12 + +* Mon Nov 01 2021 Karolina Surma - 3.10-13 +- Fix multiline arguments processing for %%py_check_import +Resolves: rhbz#2018809 +- Fix %%py_shebang_flags handling within %%py_check_import +Resolves: rhbz#2018615 +- Process .pth files in buildroot's sitedirs in %%py_check_import +Resolves: rhbz#2018551 +- Move import_all_modules.py from python-srpm-macros to python-rpm-macros + +* Mon Oct 25 2021 Karolina Surma - 3.10-12 +- Introduce -f (read from file) option to %%py{3}_check_import +- Introduce -t (filter top-level modules) option to %%py{3}_check_import +- Introduce -e (exclude module globs) option to %%py{3}_check_import + +* Wed Oct 20 2021 Tomas Orsava - 3.10-11 +- Define a new macros %%python_wheel_dir and %%python_wheel_pkg_prefix + +* Tue Oct 12 2021 Lumír Balhar - 3.10-10 +- Non-existing path in py_reproducible_pyc_path causes build to fail +Resolves: rhbz#2011056 + +* Thu Sep 09 2021 Miro Hrončok - 3.10-9 +- Set $RPM_BUILD_ROOT in %%{python3_...} macros + to allow selecting alternate sysconfig install scheme based on that variable + +* Thu Sep 09 2021 Petr Viktorin - 3.10-8 +- Use --hardlink-dupes in %%py_byte_compile and brp-python-bytecompile + (for Python 3) +- Resolves: rhbz#1977895 + +* Fri Jul 23 2021 Fedora Release Engineering - 3.9-7 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_35_Mass_Rebuild + +* Wed Jul 07 2021 Miro Hrončok - 3.10-6 +- Move Python related BuildRoot Policy scripts from redhat-rpm-config to python-srpm-macros + +* Wed Jul 07 2021 Miro Hrončok - 3.10-5 +- Introduce %%py3_check_import + +* Wed Jun 30 2021 Miro Hrončok - 3.10-4 +- Include brp-python-hardlink in python-srpm-macros since it is no longer in RPM 4.17+ + +* Mon Jun 28 2021 Miro Hrončok - 3.10-3 +- %%pytest: Set $PYTEST_ADDOPTS when %%{__pytest_addopts} is defined +- Related: rhzb#1935212 + +* Tue Jun 15 2021 Miro Hrončok - 3.10-2 +- Fix %%python_provide when fed python3.10-foo to obsolete python-foo instead of python--foo + +* Tue Jun 01 2021 Miro Hrončok - 3.10-1 +- Update main Python to Python 3.10 +- https://fedoraproject.org/wiki/Changes/Python3.10 + +* Tue Apr 27 2021 Miro Hrončok - 3.9-38 +- Escape %% symbols in macro files comments +- Fixes: rhbz#1953910 + +* Wed Apr 07 2021 Karolina Surma - 3.9-37 +- Use sysconfig.get_path() to get %%python3_sitelib and %%python3_sitearch +- Fixes: rhbz#1946972 + +* Mon Mar 29 2021 Miro Hrončok - 3.9-36 +- Allow commas as argument separator for extras names in %%python_extras_subpkg +- Fixes: rhbz#1936486 + +* Sat Feb 20 2021 Miro Hrončok - 3.9-35 +- Fix %%python_extras_subpkg with underscores in extras names + +* Mon Feb 08 2021 Miro Hrončok - 3.9-34 +- Remove python2-rpm-macros +- https://fedoraproject.org/wiki/Changes/Disable_Python_2_Dist_RPM_Generators_and_Freeze_Python_2_Macros + +* Fri Feb 05 2021 Miro Hrončok - 3.9-13 +- Automatically word-wrap the description of extras subpackages +- Fixes: rhbz#1922442 + +* Wed Jan 27 2021 Fedora Release Engineering - 3.9-12 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild + +* Tue Dec 08 2020 Miro Hrončok - 3.9-11 +- Support defining %%py3_shebang_flags to %%nil + +* Mon Sep 14 2020 Miro Hrončok - 3.9-10 +- Add %%python3_platform_triplet and %%python3_ext_suffix +- https://fedoraproject.org/wiki/Changes/Python_Upstream_Architecture_Names + +* Fri Jul 24 2020 Lumír Balhar - 3.9-9 +- Adapt %%py[3]_shebang_fix to use versioned pathfixX.Y.py + +* Fri Jul 24 2020 Lumír Balhar - 3.9-8 +- Disable Python hash seed randomization in %%py_byte_compile + +* Tue Jul 21 2020 Lumír Balhar - 3.9-7 +- Make %%py3_dist respect %%python3_pkgversion + +* Thu Jul 16 2020 Miro Hrončok - 3.9-6 +- Make the unversioned %%__python macro error +- https://fedoraproject.org/wiki/Changes/PythonMacroError +- Make %%python macros more consistent with %%python3 macros +- Define %%python_platform (as a Python version agnostic option to %%python3_platform) +- Add --no-index --no-warn-script-location pip options to %%pyX_install_wheel + +* Wed Jul 08 2020 Miro Hrončok - 3.9-5 +- Introduce %%python_extras_subpkg +- Adapt %%py_dist_name to keep square brackets +- https://fedoraproject.org/wiki/Changes/PythonExtras + +* Tue Jun 16 2020 Lumír Balhar - 3.9-4 +- Use compileall from stdlib for Python >= 3.9 + +* Thu Jun 11 2020 Miro Hrončok - 3.9-3 +- Allow to combine %%pycached with other macros (e.g. %%exclude or %%ghost) (#1838992) + +* Sat May 30 2020 Miro Hrončok - 3.9-2 +- Require the exact same version-release of other subpackages of this package + +* Thu May 21 2020 Miro Hrončok - 3.9-1 +- https://fedoraproject.org/wiki/Changes/Python3.9 +- Switch the %%py_dist_name macro to convert dots (".") into dashes as defined in PEP 503 (#1791530) + +* Mon May 11 2020 Miro Hrončok - 3.8-8 +- Implement %%pytest +- Implement %%pyX_shebang_fix +- Strip tildes from %%version in %%pypi_source by default + +* Thu May 07 2020 Miro Hrončok - 3.8-7 +- Change %%__default_python3_pkgversion from 38 to 3.8 + +* Tue May 05 2020 Miro Hrončok - 3.8-6 +- Require recent enough SRPM macros from RPM macros, to prevent missing Lua files + +* Tue May 05 2020 Miro Hrončok - 3.8-5 +- Implement %%py_provides + +* Mon May 04 2020 Tomas Hrnciar - 3.8-4 +- Make %%py3_install_wheel macro remove direct_url.json file created by PEP 610. +- https://discuss.python.org/t/pep-610-usage-guidelines-for-linux-distributions/4012 + +* Mon Apr 27 2020 Miro Hrončok - 3.8-3 +- Make pythonX-rpm-macros depend on python-rpm-macros (#1827811) + +* Tue Mar 31 2020 Lumír Balhar - 3.8-2 +- Update of bundled compileall2 module to 0.7.1 (bugfix release) + +* Mon Mar 23 2020 Miro Hrončok - 3.8-1 +- Hardcode the default Python 3 version in the SRPM macros (#1812087) +- Provide python38-foo for python3-foo and the other way around (future RHEL compatibility) +- %%python_provide: Allow any names starting with "python" or "pypy" + +* Mon Feb 10 2020 Miro Hrončok - 3-54 +- Update of bundled compileall2 module to 0.7.0 + Adds the optional --hardlink-dupes flag for compileall2 for pyc deduplication + +* Thu Feb 06 2020 Miro Hrončok - 3-53 +- Define %%py(2|3)?_shbang_opts_nodash to be used with pathfix.py -a + +* Thu Jan 30 2020 Fedora Release Engineering - 3-52 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild + +* Sat Dec 28 2019 Miro Hrončok - 3-51 +- Define %%python, but make it work only if %%__python is redefined +- Add the %%pycached macro +- Remove stray __pycache__ directory from /usr/bin when running %%py_install, + %%py_install_wheel and %%py_build_wheel macros + +* Tue Nov 26 2019 Lumír Balhar - 3-50 +- Update of bundled compileall2 module + +* Fri Sep 27 2019 Miro Hrončok - 3-49 +- Define %%python2 and %%python3 + +* Mon Aug 26 2019 Miro Hrončok - 3-48 +- Drop --strip-file-prefix option from %%pyX_install_wheel macros, it is not needed + +* Fri Jul 26 2019 Fedora Release Engineering - 3-47 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild + +* Fri Jul 12 2019 Miro Hrončok - 3-46 +- %%python_provide: Switch python2 and python3 behavior +- https://fedoraproject.org/wiki/Changes/Python_means_Python3 +- Use compileall2 module for byte-compilation with Python >= 3.4 +- Do not allow passing arguments to Python during byte-compilation +- Use `-s` argument for Python during byte-compilation + +* Tue Jul 09 2019 Miro Hrončok - 3-45 +- %%python_provide: Don't try to obsolete %%_isa provides + +* Mon Jun 17 2019 Miro Hrončok - 3-44 +- Make %%__python /usr/bin/python once again until we are ready + +* Mon Jun 10 2019 Miro Hrončok - 3-43 +- Define %%python_sitelib, %%python_sitearch, %%python_version, %%python_version_nodots, + in rpm 4.15 those are no longer defined, the meaning of python is derived from %%__python. +- Usage of %%__python or the above-mentioned macros will error unless user defined. +- The %%python_provide macro no longer gives the arched provide for arched packages (#1705656) + +* Sat Feb 02 2019 Fedora Release Engineering - 3-42 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild + +* Thu Dec 20 2018 Igor Gnatenko - 3-41 +- Add %%python_disable_dependency_generator + +* Wed Dec 05 2018 Miro Hrončok - 3-40 +- Workaround leaking buildroot PATH in %%py_byte_compile (#1647212) + +* Thu Nov 01 2018 Petr Viktorin - 3-39 +- Move "sleep 1" workaround from py3_build to py2_build (#1644923) + +* Thu Sep 20 2018 Tomas Orsava - 3-38 +- Move the __python2/3 macros to the python-srpm-macros subpackage +- This facilitates using the %%{__python2/3} in Build/Requires + +* Wed Aug 15 2018 Miro Hrončok - 3-37 +- Make %%py_byte_compile terminate build on SyntaxErrors (#1616219) + +* Wed Aug 15 2018 Miro Hrončok - 3-36 +- Make %%py_build wokr if %%__python is defined to custom value + +* Sat Jul 28 2018 Igor Gnatenko - 3-35 +- Change way how enabling-depgen works internally + +* Sat Jul 14 2018 Tomas Orsava - 3-34 +- macros.pybytecompile: Detect Python version through sys.version_info instead + of guessing from the executable name + +* Sat Jul 14 2018 Fedora Release Engineering - 3-33 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild + +* Tue Jul 10 2018 Tomas Orsava - 3-32 +- Fix %%py_byte_compile macro: when invoked with a Python 2 binary it also + mistakenly ran py3_byte_compile + +* Tue Jul 03 2018 Miro Hrončok - 3-31 +- Add %%python3_platform useful for PYTHONPATH on arched builds + +* Mon Jun 18 2018 Jason L Tibbitts III - 3-30 +- Add %%pypi_source macro, as well as %%__pypi_url and + %%_pypi_default_extension. + +* Wed Apr 18 2018 Miro Hrončok - 3-29 +- move macros.pybytecompile from python3-devel + +* Fri Apr 06 2018 Tomas Orsava - 3-28 +- Fix the %%py_dist_name macro to not convert dots (".") into dashes, so that + submodules can be addressed as well +Resolves: rhbz#1564095 + +* Fri Mar 23 2018 Miro Hrončok - 3-27 +- make LDFLAGS propagated whenever CFLAGS are + +* Fri Feb 09 2018 Fedora Release Engineering - 3-26 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild + +* Fri Jan 19 2018 Igor Gnatenko - 3-25 +- Add %%python_enable_dependency_generator + +* Tue Nov 28 2017 Tomas Orsava - 3-24 +- Remove platform-python macros (https://fedoraproject.org/wiki/Changes/Platform_Python_Stack) + +* Thu Oct 26 2017 Ville Skyttä - 3-23 +- Use -Es/-I to invoke macro scriptlets (#1506355) + +* Wed Aug 02 2017 Tomas Orsava - 3-22 +- Add platform-python macros (https://fedoraproject.org/wiki/Changes/Platform_Python_Stack) + +* Thu Jul 27 2017 Fedora Release Engineering - 3-21 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild + +* Fri Mar 03 2017 Michal Cyprian - 3-20 +- Revert "Switch %%__python3 to /usr/libexec/system-python" + after the Fedora Change https://fedoraproject.org/wiki/Changes/Making_sudo_pip_safe + was postponed + +* Fri Feb 17 2017 Michal Cyprian - 3-19 +- Switch %%__python3 to /usr/libexec/system-python + +* Sat Feb 11 2017 Fedora Release Engineering - 3-18 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild + +* Mon Jan 23 2017 Michal Cyprian - 3-17 +- Add --no-deps option to py_install_wheel macros + +* Tue Jan 17 2017 Tomas Orsava - 3-16 +- Added macros for Build/Requires tags using Python dist tags: + https://fedoraproject.org/wiki/Changes/Automatic_Provides_for_Python_RPM_Packages + +* Thu Nov 24 2016 Orion Poplawski 3-15 +- Make expanded macros start on the same line as the macro + +* Wed Nov 16 2016 Orion Poplawski 3-14 +- Fix %%py3_install_wheel (bug #1395953) + +* Wed Nov 16 2016 Orion Poplawski 3-13 +- Add missing sleeps to other build macros +- Fix build_egg macros +- Add %%py_build_wheel and %%py_install_wheel macros + +* Tue Nov 15 2016 Orion Poplawski 3-12 +- Add %%py_build_egg and %%py_install_egg macros +- Allow multiple args to %%py_build/install macros +- Tidy up macro formatting + +* Wed Aug 24 2016 Orion Poplawski 3-11 +- Use %%rpmmacrodir + +* Tue Jul 12 2016 Orion Poplawski 3-10 +- Do not generate useless Obsoletes with %%{?_isa} + +* Fri May 13 2016 Orion Poplawski 3-9 +- Make python-rpm-macros require python-srpm-macros (bug #1335860) + +* Thu May 12 2016 Jason L Tibbitts III - 3-8 +- Add single-second sleeps to work around setuptools bug. + +* Thu Feb 04 2016 Fedora Release Engineering - 3-7 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_24_Mass_Rebuild + +* Thu Jan 14 2016 Orion Poplawski 3-6 +- Fix typo in %%python_provide + +* Thu Jan 14 2016 Orion Poplawski 3-5 +- Handle noarch python sub-packages (bug #1290900) + +* Wed Jan 13 2016 Orion Poplawski 3-4 +- Fix python2/3-rpm-macros package names + +* Thu Jan 7 2016 Orion Poplawski 3-3 +- Add empty %%prep and %%build + +* Mon Jan 4 2016 Orion Poplawski 3-2 +- Combined package + +* Wed Dec 30 2015 Orion Poplawski 3-1 +- Initial package