From 31cd5dda81cee2b4b24794c55cf0230c6dd139bd Mon Sep 17 00:00:00 2001 From: MSVSphere Packaging Team Date: Wed, 15 Mar 2023 20:13:01 +0300 Subject: [PATCH] import python-rpm-macros-3.9-52.el9 --- .gitignore | 0 .python-rpm-macros.metadata | 0 SOURCES/compileall2.py | 515 ++++++++++++++++++++++++++++++++++ SOURCES/import_all_modules.py | 171 +++++++++++ SOURCES/macros.pybytecompile | 52 ++++ SOURCES/macros.python | 138 +++++++++ SOURCES/macros.python-srpm | 253 +++++++++++++++++ SOURCES/macros.python3 | 117 ++++++++ SOURCES/python.lua | 110 ++++++++ SPECS/python-rpm-macros.spec | 461 ++++++++++++++++++++++++++++++ 10 files changed, 1817 insertions(+) create mode 100644 .gitignore create mode 100644 .python-rpm-macros.metadata create mode 100644 SOURCES/compileall2.py create mode 100644 SOURCES/import_all_modules.py create mode 100644 SOURCES/macros.pybytecompile create mode 100644 SOURCES/macros.python create mode 100644 SOURCES/macros.python-srpm create mode 100644 SOURCES/macros.python3 create mode 100644 SOURCES/python.lua create mode 100644 SPECS/python-rpm-macros.spec 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/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..b58fff4 --- /dev/null +++ b/SOURCES/macros.pybytecompile @@ -0,0 +1,52 @@ +# 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 + +%py_byte_compile()\ +py2_byte_compile () {\ + python_binary="env 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\ +}\ +\ +py3_byte_compile () {\ + python_binary="env PYTHONHASHSEED=0 %1"\ + bytecode_compilation_path="%2"\ + PYTHONPATH="%{_rpmconfigdir}/redhat" $python_binary -s -B -m compileall2 -o 0 -o 1 -s $RPM_BUILD_ROOT -p / $bytecode_compilation_path \ +}\ +\ +py39_byte_compile () {\ + python_binary="env PYTHONHASHSEED=0 %1"\ + bytecode_compilation_path="%2"\ + $python_binary -s -B -m compileall -o 0 -o 1 -s $RPM_BUILD_ROOT -p / $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 \ +# 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 \ +if [ "$python_version" -ge 39 ]; then \ +py39_byte_compile "%1" "%2"; \ +elif [ "$python_version" -ge 34 ]; then \ +py3_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..f0d431d --- /dev/null +++ b/SOURCES/macros.python @@ -0,0 +1,138 @@ +# 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 +%python_sitelib %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_path('purelib'))") +%python_sitearch %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_path('platlib'))") +%python_version %(RPM_BUILD_ROOT= %{__python} -Esc "import sys; sys.stdout.write('{0.major}.{0.minor}'.format(sys.version_info))") +%python_version_nodots %(RPM_BUILD_ROOT= %{__python} -Esc "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))") +%python_platform %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_platform())") +%python_platform_triplet %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_config_var('MULTIARCH'))") +%python_ext_suffix %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))") + +%py_setup setup.py +%py_shbang_opts -s +%py_shbang_opts_nodash %(opts=%{py_shbang_opts}; echo ${opts#-}) +%py_shebang_flags %(opts=%{py_shbang_opts}; echo ${opts#-}) +%py_shebang_fix %{expand:\\\ + if [ -f /usr/bin/pathfix%{python_version}.py ]; then + pathfix=/usr/bin/pathfix%{python_version}.py + else + # older versions of Python don't have it and must BR /usr/bin/pathfix.py from python3-devel explicitly + pathfix=/usr/bin/pathfix.py + fi + if [ -z "%{?py_shebang_flags}" ]; then + shebang_flags="-k" + else + shebang_flags="-ka%{py_shebang_flags}" + fi + $pathfix -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_egg() %{expand:\\\ + CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\ + %{__python} %{py_setup} %{?py_setup_args} bdist_egg %{?*} +} + +%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} %{?*} + rm -rfv %{buildroot}%{_bindir}/__pycache__ +} + +%py_install_egg() %{expand:\\\ + mkdir -p %{buildroot}%{python_sitelib} + %{__python} -m easy_install -m --prefix %{buildroot}%{_prefix} -Z dist/*-py%{python_version}.egg %{?*} + rm -rfv %{buildroot}%{_bindir}/__pycache__ +} + +%py_install_wheel() %{expand:\\\ + %{__python} -m pip install -I dist/%{1} --root %{buildroot} --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 +} + +%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..b5a7d54 --- /dev/null +++ b/SOURCES/macros.python-srpm @@ -0,0 +1,253 @@ +# 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.9 +# +# 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 + +# === 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 python39-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:F) %{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 value_n = rpm.expand('%{-n*}') + local value_i = rpm.expand('%{-i*}') + local value_f = rpm.expand('%{-f*}') + local value_F = rpm.expand('%{-F}') + 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 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 + for i, line in ipairs({pkgdef, summary, requires, description, files, ''}) do + print(line .. '\\\n') + end + end +}} diff --git a/SOURCES/macros.python3 b/SOURCES/macros.python3 new file mode 100644 index 0000000..0ccafd9 --- /dev/null +++ b/SOURCES/macros.python3 @@ -0,0 +1,117 @@ +# 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 +%python3_sitelib %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_path('purelib'))") +%python3_sitearch %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_path('platlib'))") +%python3_version %(RPM_BUILD_ROOT= %{__python3} -Ic "import sys; sys.stdout.write('{0.major}.{0.minor}'.format(sys.version_info))") +%python3_version_nodots %(RPM_BUILD_ROOT= %{__python3} -Ic "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))") +%python3_platform %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_platform())") +%python3_platform_triplet %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_config_var('MULTIARCH'))") +%python3_ext_suffix %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))") +%py3dir %{_builddir}/python3-%{name}-%{version}-%{release} + +%py3_shbang_opts -s +%py3_shbang_opts_nodash %(opts=%{py3_shbang_opts}; echo ${opts#-}) +%py3_shebang_flags %(opts=%{py3_shbang_opts}; echo ${opts#-}) +%py3_shebang_fix %{expand:\\\ + if [ -f /usr/bin/pathfix%{python3_version}.py ]; then + pathfix=/usr/bin/pathfix%{python3_version}.py + else + # older versions of Python don't have it and must BR /usr/bin/pathfix.py from python3-devel explicitly + pathfix=/usr/bin/pathfix.py + fi + if [ -z "%{?py3_shebang_flags}" ]; then + shebang_flags="-k" + else + shebang_flags="-ka%{py3_shebang_flags}" + fi + $pathfix -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_egg() %{expand:\\\ + CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\ + %{__python3} %{py_setup} %{?py_setup_args} bdist_egg %{?*} +} + +%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} %{?*} + rm -rfv %{buildroot}%{_bindir}/__pycache__ +} + +%py3_install_egg() %{expand:\\\ + mkdir -p %{buildroot}%{python3_sitelib} + %{__python3} -m easy_install -m --prefix %{buildroot}%{_prefix} -Z dist/*-py%{python3_version}.egg %{?*} + rm -rfv %{buildroot}%{_bindir}/__pycache__ +} + +%py3_install_wheel() %{expand:\\\ + %{__python3} -m pip install -I dist/%{1} --root %{buildroot} --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") + print("\\n" .. dirname .. "__pycache__/" .. modulename .. ".cpython-3" .. pyminor .. "{,.opt-?}.pyc") + end +} + +# 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 %{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} diff --git a/SOURCES/python.lua b/SOURCES/python.lua new file mode 100644 index 0000000..5264e59 --- /dev/null +++ b/SOURCES/python.lua @@ -0,0 +1,110 @@ +-- 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 -> python3X-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 + if only_3_to_3_X then + -- Here we hardcode the xy prefix we want to obsolete to "39", because: + -- 1. Python 3.9 will remain the main Python version in RHEL 9 + -- 2. python39 in RHEL 8 is still using the dotless naming (as opposed to + -- python3.9) + xy = "39" + else + xy = rpm.expand('%{__default_python3_pkgversion}') + end + 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..2200ed5 --- /dev/null +++ b/SPECS/python-rpm-macros.spec @@ -0,0 +1,461 @@ +Name: python-rpm-macros +Version: 3.9 +Release: 52%{?dist} +Summary: The common Python RPM macros +URL: https://src.fedoraproject.org/rpms/python-rpm-macros/ + +# macros and lua: MIT +# import_all_modules.py: MIT +# compileall2.py: PSFv2 +License: MIT and Python + +# 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 + +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 +Requires: redhat-rpm-config + +# 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} + +# We obsolete the old python39-rpm-macros for a smoother upgrade from RHEL8. +# Since python39-rpm-macros are built from the python39 component in RHEL 8, +# they're fully versioned (currently `0:3.9.7`), with the patch version likely +# to increase in the future. RPM sorts this number as higher than `3.9`, which +# is the version we have in RHEL 9. Therefore we're obsoleting with an Epoch 1 +# so that all versions from RHEL 8 are obsoleted (but we keep the possibility +# of increasing the epoch in RHEL 8 to stop this). +Obsoletes: python39-rpm-macros < 1:%{version}-%{release} + +%description -n python3-rpm-macros +RPM macros for building Python 3 packages. + + +%prep +%autosetup -c -T +cp -a %{sources} . + + +%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 import_all_modules.py %{buildroot}%{_rpmconfigdir}/redhat/ + + +%check +# no macros in comments +! grep -E '^#[^%%]*%%[^%%]' %{buildroot}%{rpmmacrodir}/macros.* + + +%files +%{rpmmacrodir}/macros.python +%{rpmmacrodir}/macros.pybytecompile +%{_rpmconfigdir}/redhat/import_all_modules.py + +%files -n python-srpm-macros +%{rpmmacrodir}/macros.python-srpm +%{_rpmconfigdir}/redhat/compileall2.py +%{_rpmluadir}/fedora/srpm/python.lua + +%files -n python3-rpm-macros +%{rpmmacrodir}/macros.python3 + + +%changelog +* Wed Mar 15 2023 MSVSphere Packaging Team - 3.9-52 +- Rebuilt for MSVSphere 9.1. + +* Tue Feb 08 2022 Tomas Orsava - 3.9-52 +- %%py_provides: Do not generate Obsoletes for names containing parentheses +- Related: rhbz#1990421 + +* Tue Feb 08 2022 Tomas Orsava - 3.9-51 +- Add Obsoletes tags with the python39- prefix for smoother upgrade from RHEL8 +- Related: rhbz#1990421 + +* Tue Feb 01 2022 Miro Hrončok - 3.9-50 +- Explicitly opt-out from Python name-based provides and obsoletes generators + +* Wed Jan 19 2022 Tomas Orsava - 3.9-49 +- 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 +- Resolves: rhbz#1990421 + +* Tue Dec 21 2021 Karolina Surma - 3.9-48 +- Fix CI test configuration, so that pytest can import the package code + +* Wed Dec 08 2021 Miro Hrončok - 3.9-47 +- 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.9-46 +- Fix multiline arguments processing for %%py_check_import +- Fix %%py_shebang_flags handling within %%py_check_import +- Process .pth files in buildroot's sitedirs in %%py_check_import +- Move import_all_modules.py from python-srpm-macros to python-rpm-macros + +* Mon Oct 25 2021 Karolina Surma - 3.9-45 +- 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 + +* Thu Sep 09 2021 Miro Hrončok - 3.9-44 +- Set $RPM_BUILD_ROOT in %%{python3_...} macros + to allow selecting alternate sysconfig install scheme based on that variable + +* Wed Aug 11 2021 Tomas Orsava - 3.9-43 +- Define a new macros %%python_wheel_dir and %%python_wheel_pkg_prefix +- Related: rhbz#1982668 + +* Tue Aug 10 2021 Mohan Boddu - 3.9-42 +- Rebuilt for IMA sigs, glibc 2.34, aarch64 flags + Related: rhbz#1991688 + +* Wed Jul 07 2021 Miro Hrončok - 3.9-41 +- Introduce %%py3_check_import + +* Mon Jun 28 2021 Miro Hrončok - 3.9-40 +- %%pytest: Set $PYTEST_ADDOPTS when %%{__pytest_addopts} is defined + +* Tue Jun 15 2021 Miro Hrončok - 3.9-39 +- Fix %%python_provide when fed python3.10-foo to obsolete python-foo instead of python--foo + +* Tue Apr 27 2021 Miro Hrončok - 3.9-38 +- Escape %% symbols in macro files comments +- Fixes: rhbz#1953910 + +* Fri Apr 16 2021 Karolina Surma - 3.9-37 +- Use sysconfig.get_path() to get %%python3_sitelib and %%python3_sitearch +- Fixes: rhbz#1947468 + +* Fri Apr 16 2021 Miro Hrončok - 3.9-36.1 +- Allow commas as argument separator for extras names in %%python_extras_subpkg +- Fixes: rhbz#1936486 + +* Fri Apr 16 2021 Mohan Boddu - 3.9-36 +- Rebuilt for RHEL 9 BETA on Apr 15th 2021. Related: rhbz#1947937 + +* 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