Compare commits

..

10 commits

Author SHA1 Message Date
Miro Hrončok
a1861889d3 Add %python3_platform_triplet and %python3_ext_suffix
Also add %python_platform_triplet, %python_ext_suffix and %python_platform.

The CI tests are limited to x86_64 for now.

https://fedoraproject.org/wiki/Changes/Python_Upstream_Architecture_Names
2020-09-25 09:49:36 +02:00
Miro Hrončok
6186b94b6e Allow to combine %pycached with other macros (e.g. %exclude or %ghost)
Previous implementation allowed for only one argument to be passed to
the %pycached macro, which made it impossible to combine it with other macros.

Current implementation allows to pass other macros as arguments to
%pycached.

Example:

    %pycached %exclude /path/to/foo.py

For macro expansion limitations, the opposite order is not possible.
That is to be documented in the guidelines:
https://pagure.io/packaging-committee/pull-request/986

Added some tests.

Resolves https://bugzilla.redhat.com/show_bug.cgi?id=1838992

Co-authored-by: Marcel Plch <mplch@redhat.com>
2020-06-15 12:13:38 +02:00
Miro Hrončok
8b79a441ad Implement %pyX_shebang_fix
See https://lists.fedoraproject.org/archives/list/python-devel@lists.fedoraproject.org/thread/UGCMDDG3S32U7JJK36OEZNHLUVQQRG3M/
2020-05-25 10:47:10 +02:00
Miro Hrončok
c3cafa3dd7 Strip tildes from %version in %pypi_source by default, add tests 2020-05-25 10:47:10 +02:00
Miro Hrončok
70fa6a9627 Implement %pytest
See https://lists.fedoraproject.org/archives/list/python-devel@lists.fedoraproject.org/thread/XLPDSH362PJKMJCAYOXNJNV53Y66EF6B/
2020-05-25 10:47:10 +02:00
Miro Hrončok
ead8ec025b Backport simplified %py_provides
This is a smallest possible backport of https://src.fedoraproject.org/rpms/python-rpm-macros/pull-request/52

Notable simplifications:

- there is no Lua library
- %python_provide remains untouched and the logic is not shared with %py_provides
- there are no pythonXY-foo provides on Fedora < 33
2020-05-25 10:47:08 +02:00
Miro Hrončok
18667277d5 Make pythonX-rpm-macros depend on python-rpm-macros
%pyX_(build|install) uses %py_setup from python-rpm-macros

Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1827811
2020-04-28 14:18:47 +02:00
Lumir Balhar
3f331a6ad1 Update of bundled compileall2 module to 0.7.1 (bugfix release) 2020-03-31 14:25:45 +02:00
Miro Hrončok
5914a75181 Update of bundled compileall2 module to 0.7.0
Adds the optional --hardlink-dupes flag for compileall2 for pyc deduplication

This is explained in https://discuss.python.org/t/3014
                 and https://github.com/fedora-python/compileall2/issues/16

This option is not yet used anywhere. That allows us to backport this to all
Fedoras but only use --hardlink-dupes on rawhide first.
2020-02-10 23:58:58 +01:00
Miro Hrončok
f43227fac0 Define %py(2|3)?_shbang_opts_nodash to be used with pathfix.py -a 2020-02-07 18:07:36 +01:00
25 changed files with 337 additions and 3469 deletions

View file

@ -1 +0,0 @@
1

View file

@ -1,18 +0,0 @@
#!/bin/bash -eu
# If using normal root, avoid changing anything.
if [[ -z "${RPM_BUILD_ROOT:-}" ]] || [[ "${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
find "$path_to_fix" -type f -name '*.pyc' -exec /usr/bin/marshalparser --fix --overwrite '{}' '+'

View file

@ -1,150 +0,0 @@
#!/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" ]] || [[ "$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 # No longer used
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
# 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
# -e excludes symbolic links pointing outside the build root
# -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 -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"
class Filter:
def search(self, path):
ret = not os.path.realpath(path).startswith(build_root)
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 ]] && [[ 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 ]] && [[ 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 ]] && [[ 0"$errors_terminate" -ne 0 ]]; then
# One or more of the files had a syntax error
exit 1
fi
done

View file

@ -1,25 +0,0 @@
#!/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

View file

@ -1,15 +0,0 @@
#!/usr/bin/bash
set -eu
# If using normal root, avoid changing anything.
if [[ "${RPM_BUILD_ROOT:-/}" = "/" ]] ; then
exit 0
fi
find "$RPM_BUILD_ROOT" -name 'INSTALLER' -type f -print0|grep -z -E "/usr/lib(64)?/python3\.[0-9]+/site-packages/[^/]+\.dist-info/INSTALLER" | while read -d "" installer ; do
if cmp -s <(echo pip) "$installer" ; then
echo "rpm" > "$installer"
rm -f "$(dirname "$installer")/RECORD"
fi
done
exit 0

View file

@ -1,163 +0,0 @@
"""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)

View file

@ -4,7 +4,7 @@ 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, it compiles all modules on sys.path, without
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.)
@ -36,11 +36,11 @@ PY35 = sys.version_info[0:2] >= (3, 5)
# introduced in Python 3.7. These cases are covered by variables here or by PY37
# variable itself.
if PY37:
pyc_struct_format = '<4sLL'
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_struct_format = '<4sl'
pyc_header_lenght = 8
pyc_header_format = (pyc_struct_format, importlib.util.MAGIC_NUMBER)
@ -106,7 +106,7 @@ def compile_dir(dir, maxlevels=None, ddir=None, force=False,
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 beginning of original file path, applied
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
@ -120,34 +120,23 @@ def compile_dir(dir, maxlevels=None, ddir=None, force=False,
stripdir = dir
prependdir = ddir
ddir = None
if workers < 0:
raise ValueError('workers must be greater or equal to 0')
if workers != 1:
# Check if this is a system where ProcessPoolExecutor can function.
from concurrent.futures.process import _check_system_limits
try:
_check_system_limits()
except NotImplementedError:
workers = 1
else:
from concurrent.futures import ProcessPoolExecutor
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 != 1 and ProcessPoolExecutor is not None:
mp_context_arg = {}
if PY37:
import multiprocessing
if multiprocessing.get_start_method() == 'fork':
mp_context = multiprocessing.get_context('forkserver')
else:
mp_context = None
mp_context_arg = {"mp_context": mp_context}
# If workers == 0, let ProcessPoolExecutor choose
if workers is not None and workers != 1 and ProcessPoolExecutor is not None:
workers = workers or None
with ProcessPoolExecutor(max_workers=workers,
**mp_context_arg) as executor:
with ProcessPoolExecutor(max_workers=workers) as executor:
results = executor.map(partial(compile_file,
ddir=ddir, force=force,
rx=rx, quiet=quiet,
@ -189,7 +178,7 @@ def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0,
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 beginning of original file path, applied
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.
@ -201,8 +190,10 @@ def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0,
"in combination with stripdir or prependdir"))
success = True
fullname = os.fspath(fullname)
stripdir = os.fspath(stripdir) if stripdir is not None else None
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
@ -215,13 +206,13 @@ def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0,
if stripdir is not None:
fullname_parts = fullname.split(os.path.sep)
stripdir_parts = stripdir.split(os.path.sep)
ddir_parts = list(fullname_parts)
if stripdir_parts != fullname_parts[:len(stripdir_parts)]:
if quiet < 2:
print("The stripdir path {!r} is not a valid prefix for "
"source path {!r}; ignoring".format(stripdir, fullname))
else:
dfile = os.path.join(*fullname_parts[len(stripdir_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:
@ -267,7 +258,7 @@ def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0,
if not force:
try:
mtime = int(os.stat(fullname).st_mtime)
expect = struct.pack(*(pyc_header_format + (mtime & 0xFFFF_FFFF,)))
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)
@ -310,8 +301,9 @@ def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0,
else:
print('*** ', end='')
# escape non-printable characters in msg
encoding = sys.stdout.encoding or sys.getdefaultencoding()
msg = err.msg.encode(encoding, errors='backslashreplace').decode(encoding)
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
@ -416,8 +408,8 @@ def main():
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 the optimization level '
'of the Python interpreter itself (see -O).'))
'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',
@ -464,8 +456,7 @@ def main():
# if flist is provided then load it
if args.flist:
try:
with (sys.stdin if args.flist=='-' else
open(args.flist, encoding="utf-8")) as f:
with (sys.stdin if args.flist=='-' else open(args.flist)) as f:
for line in f:
compile_dests.append(line.strip())
except OSError:
@ -473,6 +464,9 @@ def main():
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]

View file

@ -1,188 +0,0 @@
'''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
import traceback
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.
Return a list of failed modules.
'''
failed_modules = []
for module in modules:
print('Check import:', module, file=sys.stderr)
try:
importlib.import_module(module)
except Exception:
traceback.print_exc(file=sys.stderr)
failed_modules.append(module)
if failed_modules:
print(f'Failed to import: {", ".join(failed_modules)}', file=sys.stderr)
return failed_modules
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()
failed_modules = import_modules(modules)
if failed_modules:
raise SystemExit(1)
if __name__ == '__main__':
main()

View file

@ -4,27 +4,17 @@
# Which unfortunately makes the definition more complicated than it should be
# Usage:
# %%py_byte_compile <interpereter> <path>
# %py_byte_compile <interpereter> <path>
# Example:
# %%py_byte_compile %%{__python3} %%{buildroot}%%{_datadir}/spam/plugins/
# %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 <interpereter> <path>}) || :
# 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 <interpereter> <path>}) || :
%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"\
python_binary="%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\
@ -32,38 +22,15 @@ py2_byte_compile () {\
test $failure -eq 0\
}\
\
py34_byte_compile () {\
python_binary="%{__env_unset_source_date_epoch_if_not_clamp_mtime} PYTHONHASHSEED=0 %1"\
py3_byte_compile () {\
python_binary="%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 \
PYTHONPATH="%{_rpmconfigdir}/redhat" $python_binary -s -B -m compileall2 -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 \
# 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
# compileall2 Python module is not compatible with Python < 3.4 \
if [ "$python_version" -ge 34 ]; then py3_byte_compile "%1" "%2"; else py2_byte_compile "%1" "%2"; fi

View file

@ -1,176 +1,84 @@
# 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])
}
# Deprecation wrapper, warns only once per macro
# Options:
# -n - The name of the macro that is deprecated
%_python_deprecated(n:) %{lua:
if not _python_deprecated_warned then
-- This is intentionally a global lua table
_python_deprecated_warned = {}
end
if not _python_deprecated_warned[opt.n] then
_python_deprecated_warned[opt.n] = true
local msg = "The %" .. opt.n .. " macro is deprecated and will likely stop working in Fedora 44. " ..
"See the current Python packaging guidelines: " ..
"https://docs.fedoraproject.org/en-US/packaging-guidelines/Python/"
macros.warn({msg})
end
}
# 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}
%python_sitelib %(%{__python} -Esc "from distutils.sysconfig import get_python_lib; print(get_python_lib())")
%python_sitearch %(%{__python} -Esc "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")
%python_version %(%{__python} -Esc "import sys; sys.stdout.write('{0.major}.{0.minor}'.format(sys.version_info))")
%python_version_nodots %(%{__python} -Esc "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))")
%python_platform %(%{__python} -Esc "import sysconfig; print(sysconfig.get_platform())")
%python_platform_triplet %(%{__python} -Esc "import sysconfig; print(sysconfig.get_config_var('MULTIARCH'))")
%python_ext_suffix %(%{__python} -Esc "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))")
%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 -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 [ -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}
%py_shebang_fix %{expand:/usr/bin/pathfix.py -pni %{__python} -k%{?py_shebang_flags:a %py_shebang_flags}}
# Use the slashes after expand so that the command starts on the same line as
# the macro
%py_build() %{_python_deprecated -n py_build}%{expand:\\\
%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}" %{?*}
sleep 1
}
%py_build_wheel() %{_python_deprecated -n py_build_wheel}%{expand:\\\
%py_build_egg() %{expand:\\\
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python} %{py_setup} %{?py_setup_args} bdist_egg %{?*}
sleep 1
}
%py_build_wheel() %{expand:\\\
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python} %{py_setup} %{?py_setup_args} bdist_wheel %{?*}
sleep 1
}
%py_install() %{_python_deprecated -n py_install}%{expand:\\\
%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__
%{__python} %{py_setup} %{?py_setup_args} install -O1 --skip-build --root %{buildroot} %{?*}
}
%py_install_egg() %{expand:\\\
mkdir -p %{buildroot}%{python_sitelib}
easy_install -m --prefix %{buildroot}%{_prefix} -Z dist/*-py%{python_version}.egg %{?*}
}
%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)
}
pip install -I dist/%{1} --root %{buildroot} --strip-file-prefix %{buildroot} --no-deps
}
%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
package = rpm.expand("%{?1}")
vr = rpm.expand("%{?epoch:%{epoch}:}%{version}-%{release}")
if (string.starts(package, "python2-")) then
--No unversioned provides as python2 is not default
elseif (string.starts(package, "python" .. rpm.expand("%{python3_pkgversion}") .. "-")) then
print("\\nProvides: python-")
l = 8 + string.len(rpm.expand("%{python3_pkgversion}"))
print(string.sub(package,l,string.len(package)))
print(" = ")
print(vr)
--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)
print(string.sub(package,l,string.len(package)))
print(" < ")
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
elseif (rpm.expand("%{?python3_other_pkgversion}") ~= "" and string.starts(package, "python" .. rpm.expand("%{python3_other_pkgversion}") .. "-")) then
--No unversioned provides as python3_other is not default
elseif (string.starts(package, "pypy-")) then
--No unversioned provides as pypy is not default
elseif (string.starts(package, "pypy2-")) then
--No unversioned provides as pypy is not default
elseif (string.starts(package, "pypy3-")) then
--No unversioned provides as pypy is not default
elseif (string.starts(package, "python-")) then
--No unversioned provides needed for unversioned python
else
print("%python_provide: ERROR: ")
print(package)
@ -178,16 +86,6 @@ end
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}

View file

@ -1,35 +1,3 @@
# 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.14
#
# 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
@ -37,66 +5,35 @@
# use the underscored macros to redefine the behavior of %%python3_version etc.
%__python2 /usr/bin/python2
%__python3 /usr/bin/python%{python3_pkgversion}
%__python3 /usr/bin/python3
# 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}
# For backwards compatibility only
# See the comments in https://src.fedoraproject.org/rpms/python-rpm-macros/pull-request/22
%__python /usr/bin/python
# Users can use %%python only if they redefined %%__python (e.g. to %%__python3)
%python %__python
%python() %{lua:\
__python = rpm.expand("%__python")\
if __python == "/usr/bin/python" then\
rpm.expand("%{error:Cannot use %%python if %%__python wasn't redefined to something other than /usr/bin/python.}")\
else\
print(__python)\
end\
}
# 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
# python3_pkgversion specifies the version of Python 3 in the distro. It can be
# a specific version (e.g. 34 in Fedora EPEL7)
%python3_pkgversion 3
# Set to /bin/true to avoid %ifdefs and %{? in specfiles
%__python3_other /bin/true
%py3_other_build /bin/true
%py3_other_install /bin/true
### BRP scripts (and related macros)
## Modifies installation method in .dist-info/INSTALLER file to rpm
%python_rpm_in_distinfo 1
## 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_rpm_in_distinfo %{_rpmconfigdir}/redhat/brp-python-rpm-in-distinfo
%__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-rpm-in-distinfo modifies .dist-info/INSTALLER file
# 2. brp-python-bytecompile can create (or replace) pyc files
# 3. brp-fix-pyc-reproducibility can modify the pyc files from above
# 4. brp-python-hardlink de-duplicates identical pyc files
%__os_install_post_python \
%{?python_rpm_in_distinfo:%{?__brp_python_rpm_in_distinfo}} \
%{?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 ===
@ -107,13 +44,13 @@
# Converts Python dist name to a canonical format
%py_dist_name() %{lua:\
name = rpm.expand("%{?1:%{1}}");\
canonical = string.gsub(string.lower(name), "[^%w%[%]]+", "-");\
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
# macro (%py_dist_name) overwrites them
%py2_dist() %{lua:\
args = {}\
arg = 1\
@ -133,9 +70,8 @@
# 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
# macro (%py_dist_name) overwrites them
%py3_dist() %{lua:\
python3_pkgversion = rpm.expand("%python3_pkgversion");\
args = {}\
arg = 1\
while (true) do\
@ -148,19 +84,19 @@
end\
for arg, name in ipairs(args) do\
canonical = rpm.expand("%py_dist_name " .. name);\
print("python" .. python3_pkgversion .. "dist(" .. canonical .. ") ");\
print("python3dist(" .. 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.
# 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.
# Requires %__pypi_url and %__pypi_default_extension to be defined.
%__pypi_url https://files.pythonhosted.org/packages/source/
%__pypi_default_extension tar.gz
@ -198,8 +134,6 @@
}
%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}')
@ -209,94 +143,9 @@
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
-- NB: dash needs to be escaped!
if name:match('^python3%-') then
replaced = name:gsub('^python3%-', 'python-')
print('Provides: ' .. replaced .. ' = ' .. evr .. '\\n')
end
}
%python_extras_subpkg(n:i:f:FaAv:) %{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 value_v = rpm.expand('%{-v}')
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 verrel = rpm.expand('%{?-v*}%{!?-v:%{version}-%{release}}')
local requires = 'Requires: ' .. value_n .. ' = %{?epoch:%{epoch}:}' .. verrel
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 %dir ' .. 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
}}

View file

@ -1,124 +0,0 @@
# The macros in this file are used to add SBOM to wheel files that we ship.
# Majority of Python packages will not need to do that,
# as they only use wheels as an intermediate artifact.
# The macros will be used by packages installing wheel to %%python_wheel_dir
# or by Python interpreters bundling their own (patched) wheels.
#
# The runtime dependencies are not Required by the python-rpm-macros package,
# users of this macro need to specify them on their own or rely on the fact that
# they are all available in the default buildroot.
#
# Usage: %%python_wheel_inject_sbom PATHS_TO_WHEELS
#
# The wheels are modified in-place.
# Path of the SBOM file in the PEP 770 .dist-info/sboms directory
# This filename is explicitly mentioned in https://cyclonedx.org/specification/overview/
# section Recognized file patterns
%__python_wheel_sbom_filename bom.json
# The SBOM content to put to the file
# This is a CycloneDX component as recommended in https://discuss.python.org/t/97436/7
%__python_wheel_sbom_content %{expand:{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"components": [
{
"type": "library",
"name": "%{name}",
"version": "%{version}-%{release}",
"purl": "%{__python_wheel_purl}"
}
]
}}
# The purl used above
# We use the src package name (which is easier to get and more useful to consumers).
# Note that epoch needs special handling, see https://github.com/package-url/purl-spec/issues/69
# and https://redhatproductsecurity.github.io/security-data-guidelines/purl/
%__python_wheel_purl pkg:rpm/%{__python_wheel_dist_purl_namespace}/%{name}@%{version}-%{release}?%{?epoch:epoch=%{epoch}&}arch=src
# The purl namespace used above
# https://lists.fedoraproject.org/archives/list/packaging@lists.fedoraproject.org/thread/GTRCTAF3R3SSBVEJYFCATKNRT7RYVFQI/
# Distributors, define %%dist_purl_namespace to set this.
# The rest of the code is fallback for distributions without it (relying on %%dist_name).
%__python_wheel_dist_purl_namespace %{?dist_purl_namespace}%{!?dist_purl_namespace:%{lua:
if macros.epel then
-- being epel beats the %%dist_name value
-- added in https://src.fedoraproject.org/rpms/epel-rpm-macros/pull-request/86
print("epel")
else
local dist_map = {
-- fedora is in the purl-spec examples https://github.com/package-url/purl-spec/blob/main/PURL-TYPES.rst#rpm
-- added in https://src.fedoraproject.org/rpms/fedora-release/pull-request/385
["Fedora Linux"] = "fedora",
-- added in https://gitlab.com/redhat/centos-stream/rpms/centos-stream-release/-/merge_requests/7
["CentOS Stream"] = "centos",
-- documented at https://redhatproductsecurity.github.io/security-data-guidelines/purl/
["Red Hat Enterprise Linux"] = "redhat",
-- documented at https://wiki.almalinux.org/documentation/sbom-guide.html
["AlmaLinux"] = "almalinux",
-- from https://github.com/google/osv.dev/pull/2939
["Rocky Linux"] = "rocky-linux",
}
print(dist_map[macros.dist_name] or "unknown")
end
}}
# A Bash scriptlet to inject the SBOM file into the wheel(s)
# The macro takes positional nargs+ with wheel paths
# For each wheel, it
# 1. aborts if the SBOM file is already there (it won't override)
# 2. inserts the SBOM file to .dist-info/sboms
# 3. amends .dist-info/RECORD with the added SBOM file
%python_wheel_inject_sbom() %{expand:(
%[%# ? "" : "%{error:%%%0: At least one argument (wheel path) is required}"]
set -eu -o pipefail
export LANG=C.utf-8
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
pwd0=$(pwd)
ret=0
for whl in %{*}; do
cd "$tmpdir"
if [[ "$whl" != /* ]]; then
whl="$pwd0/$whl"
fi
record=$(zipinfo -1 "$whl" | grep -E '^[^/]+-[^/]+\.dist-info/RECORD$')
distinfo="${record%%/RECORD}"
bom="$distinfo/sboms/%{__python_wheel_sbom_filename}"
if zipinfo -1 "$whl" | grep -qFx "$bom"; then
echo -e "\\n\\nERROR %%%%%0: $whl already has $bom, aborting\\n\\n" >&2
ret=1
continue
fi
unzip "$whl" "$record"
mkdir "$distinfo/sboms"
echo '%{__python_wheel_sbom_content}' > "$bom"
checksum="sha256=$(sha256sum "$bom" | cut -f1 -d' ')"
size="$(wc --bytes "$bom" | cut -f1 -d' ')"
echo "$bom,$checksum,$size" >> "$record"
if [[ -n "${SOURCE_DATE_EPOCH:-}" ]]; then
touch --date="@$SOURCE_DATE_EPOCH" "$bom" "$record"
fi
zip -r "$whl" "$record" "$bom"
rm -rf "$distinfo"
cd "$pwd0"
done
exit $ret
)}

48
macros.python2 Normal file
View file

@ -0,0 +1,48 @@
%python2_sitelib %(%{__python2} -Esc "from distutils.sysconfig import get_python_lib; print(get_python_lib())")
%python2_sitearch %(%{__python2} -Esc "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")
%python2_version %(%{__python2} -Esc "import sys; sys.stdout.write('{0.major}.{0.minor}'.format(sys.version_info))")
%python2_version_nodots %(%{__python2} -Esc "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))")
%py2_shbang_opts -s
%py2_shbang_opts_nodash %(opts=%{py2_shbang_opts}; echo ${opts#-})
%py2_shebang_flags %(opts=%{py2_shbang_opts}; echo ${opts#-})
%py2_shebang_fix %{expand:/usr/bin/pathfix.py -pni %{__python2} -k%{?py2_shebang_flags:a %py2_shebang_flags}}
# Use the slashes after expand so that the command starts on the same line as
# the macro
# The `sleep 1` commands work around a race in install; see:
# https://bugzilla.redhat.com/show_bug.cgi?id=1644923
%py2_build() %{expand:\\\
sleep 1
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python2} %{py_setup} %{?py_setup_args} build --executable="%{__python2} %{py2_shbang_opts}" %{?*}
sleep 1
}
%py2_build_egg() %{expand:\\\
sleep 1
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python2} %{py_setup} %{?py_setup_args} bdist_egg %{?*}
sleep 1
}
%py2_build_wheel() %{expand:\\\
sleep 1
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python2} %{py_setup} %{?py_setup_args} bdist_wheel %{?*}
sleep 1
}
%py2_install() %{expand:\\\
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python2} %{py_setup} %{?py_setup_args} install -O1 --skip-build --root %{buildroot} %{?*}
}
%py2_install_egg() %{expand:\\\
mkdir -p %{buildroot}%{python2_sitelib}
easy_install-%{python2_version} -m --prefix %{buildroot}%{_prefix} -Z dist/*-py%{python2_version}.egg %{?*}
}
%py2_install_wheel() %{expand:\\\
pip%{python2_version} install -I dist/%{1} --root %{buildroot} --no-deps
}

View file

@ -1,98 +1,46 @@
# 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}
%python3_sitelib %(%{__python3} -Ic "from distutils.sysconfig import get_python_lib; print(get_python_lib())")
%python3_sitearch %(%{__python3} -Ic "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")
%python3_version %(%{__python3} -Ic "import sys; sys.stdout.write('{0.major}.{0.minor}'.format(sys.version_info))")
%python3_version_nodots %(%{__python3} -Ic "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))")
%python3_platform %(%{__python3} -Ic "import sysconfig; print(sysconfig.get_platform())")
%python3_platform_triplet %(%{__python3} -Ic "import sysconfig; print(sysconfig.get_config_var('MULTIARCH'))")
%python3_ext_suffix %(%{__python3} -Ic "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))")
%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 -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 [ -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}
%py3_shebang_fix %{expand:/usr/bin/pathfix.py -pni %{__python3} -k%{?py3_shebang_flags:a %py3_shebang_flags}}
# Use the slashes after expand so that the command starts on the same line as
# the macro
%py3_build() %{_python_deprecated -n py3_build}%{expand:\\\
%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() %{_python_deprecated -n py3_build_wheel}%{expand:\\\
%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() %{_python_deprecated -n py3_install}%{expand:\\\
%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__
%{__python3} %{py_setup} %{?py_setup_args} install -O1 --skip-build --root %{buildroot} %{?*}
}
%py3_install_egg() %{expand:\\\
mkdir -p %{buildroot}%{python3_sitelib}
easy_install-%{python3_version} -m --prefix %{buildroot}%{_prefix} -Z dist/*-py%{python3_version}.egg %{?*}
}
%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)
}
pip%{python3_version} install -I dist/%{1} --root %{buildroot} --no-deps
}
# This only supports Python 3.5+ and will never work with Python 2.
@ -106,21 +54,15 @@
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:\\\
# This is intended for Python 3 only, hence also no Python version in the name.
%__pytest /usr/bin/pytest
%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_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
%__pytest}

View file

@ -1,198 +0,0 @@
#!/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')
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()

View file

@ -1,40 +0,0 @@
execute:
how: tmt
discover:
- name: same_repo
how: shell
tests:
- name: pytest
test: PYTHONPATH=/usr/lib/rpm/redhat ALTERNATE_PYTHON_VERSION=3.6 pytest -v
- name: manual_byte_compilation_clamp_mtime_off
path: /tests
test: rpmbuild --define 'dist .clamp0' --define 'clamp_mtime_to_source_date_epoch 0' -ba pythontest.spec
- name: manual_byte_compilation_clamp_mtime_on
path: /tests
test: rpmbuild --define 'dist .clamp1' --define 'clamp_mtime_to_source_date_epoch 1' -ba pythontest.spec
- name: rpmlint_clamp_mtime_off
test: rpmlint ~/rpmbuild/RPMS/x86_64/pythontest-0-0.clamp0.x86_64.rpm | grep python-bytecode-inconsistent-mtime || exit 0 && exit 1
- name: rpmlint_clamp_mtime_on
test: rpmlint ~/rpmbuild/RPMS/x86_64/pythontest-0-0.clamp1.x86_64.rpm | grep python-bytecode-inconsistent-mtime || exit 0 && exit 1
- name: python_wheel_inject_sbom
path: /tests
test: rpmbuild -ba testwheel.spec
prepare:
- name: Install dependencies
how: install
package:
- rpm-build
- rpmlint
- python-rpm-macros
- python3-rpm-macros
- python3-devel
- python3-setuptools
- python3-pip
- python3-pytest
- python3.6
- dnf
- name: Update packages
how: shell
script: dnf upgrade -y

View file

@ -1,75 +1,22 @@
Name: python-rpm-macros
Summary: The common Python RPM macros
Version: 3
Release: 59%{?dist}
Summary: The unversioned 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
Source106: macros.python-wheel-sbom
# Lua files
Source201: python.lua
# Python code
%global compileall2_version 0.8.0
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
# brp script to write "rpm" string into the .dist-info/INSTALLER file
Source404: brp-python-rpm-in-distinfo
# 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: 9%{?dist}
# macros: MIT, compileall2.py: PSFv2
License: MIT and Python
Source0: macros.python
Source1: macros.python-srpm
Source2: macros.python2
Source3: macros.python3
Source4: macros.pybytecompile
Source5: https://github.com/fedora-python/compileall2/raw/v0.7.1/compileall2.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
# For %%python3_pkgversion used in %%python_provide and compileall2.py
Requires: python-srpm-macros >= 3-46
Obsoletes: python-macros < 3
Provides: python-macros = %{version}-%{release}
%description
This package contains the unversioned Python RPM macros, that most
@ -78,393 +25,90 @@ 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}
Requires: redhat-rpm-config
%description -n python-srpm-macros
RPM macros for building Python source packages.
%package -n python2-rpm-macros
Summary: RPM macros for building Python 2 packages
Requires: python-srpm-macros >= 3-38
Requires: python-rpm-macros
# Would need to be different for each release - worth it?
#Conflicts: python2-devel < 2.7.11-3
%description -n python2-rpm-macros
RPM macros for building Python 2 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}
Requires: python-srpm-macros >= 3-38
Requires: python-rpm-macros
%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
%build
%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
install -m 644 %{SOURCE0} %{SOURCE1} %{SOURCE2} %{SOURCE3} %{SOURCE4} \
%{buildroot}%{rpmmacrodir}/
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}
%global __brp_python_rpm_in_distinfo %{add_buildroot __brp_python_rpm_in_distinfo}
%check
# no macros in comments
grep -E '^#[^%%]*%%[^%%]' %{buildroot}%{rpmmacrodir}/macros.* && exit 1 || true
install -m 644 %{SOURCE5} \
%{buildroot}%{_rpmconfigdir}/redhat/
%files
%{rpmmacrodir}/macros.python
%{rpmmacrodir}/macros.pybytecompile
%{rpmmacrodir}/macros.python-wheel-sbom
%{_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
%{_rpmconfigdir}/redhat/brp-python-rpm-in-distinfo
%{_rpmluadir}/fedora/srpm/python.lua
%files -n python2-rpm-macros
%{rpmmacrodir}/macros.python2
%files -n python3-rpm-macros
%{rpmmacrodir}/macros.python3
%changelog
* Thu Oct 16 2025 Miro Hrončok <mhroncok@redhat.com> - 3.14-9
- %%python_extras_subpkg: Only %%ghost the egg-info/dist-info directory, not the content
- That way, accidentally unpackaged files within are reported as errors
* Tue Sep 09 2025 Miro Hrončok <mhroncok@redhat.com> - 3.14-8
- %%python_extras_subpkg: Add -v option to specify the required version(-release)
- This is useful when the extras are built from a different specfile (e.g. in EPEL for a RHEL base package)
* Fri Aug 29 2025 Miro Hrončok <mhroncok@redhat.com> - 3.14-7
- %%python_wheel_inject_sbom: Don't accidentally alter nested .dist-infos
* Wed Aug 13 2025 Miro Hrončok <mhroncok@redhat.com> - 3.14-6
- Introduce %%python_wheel_inject_sbom
* Mon Aug 11 2025 Lumír Balhar <lbalhar@redhat.com> - 3.14-5
- import_all_modules: Add error handling for import failures
* Fri Jul 25 2025 Fedora Release Engineering <releng@fedoraproject.org>
- Rebuilt for https://fedoraproject.org/wiki/Fedora_43_Mass_Rebuild
* Mon Jul 21 2025 Íñigo Huguet <ihuguet@riseup.net> - 3.14-3
- pathfix.py: Don't fail on symbolic links
* Sun Jun 29 2025 Miro Hrončok <mhroncok@redhat.com> - 3.14-2
- Deprecate %%py3_build, %%py3_build_wheel, and %%py3_install
- Deprecate %%py_build, %%py_build_wheel, and %%py_install
- https://fedoraproject.org/wiki/Changes/DeprecateSetuppyMacros
* Wed May 28 2025 Karolina Surma <ksurma@redhat.com> - 3.14-1
- Update main Python to 3.14
* Mon Feb 10 2025 Tomáš Hrnčiar <thrnciar@redhat.com> - 3.13-5
- Add brp script to modify .dist-info/INSTALLER file
* Sat Jan 18 2025 Fedora Release Engineering <releng@fedoraproject.org> - 3.13-4
- Rebuilt for https://fedoraproject.org/wiki/Fedora_42_Mass_Rebuild
* Fri Jul 19 2024 Fedora Release Engineering <releng@fedoraproject.org> - 3.12-3
- Rebuilt for https://fedoraproject.org/wiki/Fedora_41_Mass_Rebuild
* Tue Jun 25 2024 Cristian Le <fedora@lecris.me> - 3.13-2
- %%python_extras_subpkg: Add option -a to include BuildArch: noarch
* Thu Jun 06 2024 Karolina Surma <ksurma@redhat.com> - 3.13-1
- Update main Python to 3.13
* Thu Mar 28 2024 Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl> - 3.12-9
- Minor improvements to brp-fix-pyc-reproducibility
* Fri Mar 22 2024 Lumír Balhar <lbalhar@redhat.com> - 3.12-8
- Update bundled compileall2 to version 0.8.0
* Thu Jan 25 2024 Miro Hrončok <mhroncok@redhat.com> - 3.12-7
- %%py3_test_envvars: Only set $PYTEST_XDIST_AUTO_NUM_WORKERS if not already set
* Mon Jan 22 2024 Fedora Release Engineering <releng@fedoraproject.org> - 3.12-6
- Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild
* Mon Oct 09 2023 Maxwell G <maxwell@gtmx.me> - 3.12-5
- Fix python macro memoizing to account for changing %%__python3
* Tue Sep 05 2023 Maxwell G <maxwell@gtmx.me> - 3.12-4
- Remove %%py3_build_egg and %%py3_install_egg macros.
* Wed Aug 09 2023 Karolina Surma <ksurma@redhat.com> - 3.12-3
- Declare the license as an SPDX expression
* Fri Jul 21 2023 Fedora Release Engineering <releng@fedoraproject.org> - 3.12-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_39_Mass_Rebuild
* Tue Jun 13 2023 Tomáš Hrnčiar <thrnciar@redhat.com> - 3.12-1
- Update main Python to Python 3.12
- https://fedoraproject.org/wiki/Changes/Python3.12
* Thu Mar 16 2023 Miro Hrončok <mhroncok@redhat.com> - 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 <mhroncok@redhat.com> - 3.11-9
- Memoize values of macros that execute python to get their value
- Fixes: rhbz#2155505
* Fri Jan 20 2023 Fedora Release Engineering <releng@fedoraproject.org> - 3.11-8
- Rebuilt for https://fedoraproject.org/wiki/Fedora_38_Mass_Rebuild
* Mon Dec 19 2022 Miro Hrončok <mhroncok@redhat.com> - 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 <mhroncok@redhat.com> - 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 <lbalhar@redhat.com> - 3.11-5
- Include pathfix.py in this package
* Fri Jul 22 2022 Fedora Release Engineering <releng@fedoraproject.org> - 3.10-4
- Rebuilt for https://fedoraproject.org/wiki/Fedora_37_Mass_Rebuild
* Tue Jul 19 2022 Miro Hrončok <mhroncok@redhat.com> - 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 <mhroncok@redhat.com> - 3.11-2
- Define %%python3_cache_tag / %%python_cache_tag, e.g. cpython-311
* Mon Jun 13 2022 Tomáš Hrnčiar <thrnciar@redhat.com> - 3.11-1
- Update main Python to Python 3.11
- https://fedoraproject.org/wiki/Changes/Python3.11
* Thu May 26 2022 Owen Taylor <otaylor@redhat.com> - 3.10-18
- Support installing to %%{_prefix} other than /usr
* Tue Feb 08 2022 Tomas Orsava <torsava@redhat.com> - 3.10-17
- %%py_provides: Do not generate Obsoletes for names containing parentheses
* Mon Jan 31 2022 Miro Hrončok <mhroncok@redhat.com> - 3.10-16
- Explicitly opt-out from Python name-based provides and obsoletes generators
* Tue Dec 21 2021 Tomas Orsava <torsava@redhat.com> - 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 <mhroncok@redhat.com> - 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 <ksurma@redhat.com> - 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 <ksurma@redhat.com> - 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 <torsava@redhat.com> - 3.10-11
- Define a new macros %%python_wheel_dir and %%python_wheel_pkg_prefix
* Tue Oct 12 2021 Lumír Balhar <lbalhar@redhat.com> - 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 <mhroncok@redhat.com> - 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 <pviktori@redhat.com> - 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 <releng@fedoraproject.org> - 3.9-7
- Rebuilt for https://fedoraproject.org/wiki/Fedora_35_Mass_Rebuild
* Wed Jul 07 2021 Miro Hrončok <mhroncok@redhat.com> - 3.10-6
- Move Python related BuildRoot Policy scripts from redhat-rpm-config to python-srpm-macros
* Wed Jul 07 2021 Miro Hrončok <mhroncok@redhat.com> - 3.10-5
- Introduce %%py3_check_import
* Wed Jun 30 2021 Miro Hrončok <mhroncok@redhat.com> - 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 <mhroncok@redhat.com> - 3.10-3
- %%pytest: Set $PYTEST_ADDOPTS when %%{__pytest_addopts} is defined
- Related: rhzb#1935212
* Tue Jun 15 2021 Miro Hrončok <mhroncok@redhat.com> - 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 <mhroncok@redhat.com> - 3.10-1
- Update main Python to Python 3.10
- https://fedoraproject.org/wiki/Changes/Python3.10
* Tue Apr 27 2021 Miro Hrončok <mhroncok@redhat.com> - 3.9-38
- Escape %% symbols in macro files comments
- Fixes: rhbz#1953910
* Wed Apr 07 2021 Karolina Surma <ksurma@redhat.com> - 3.9-37
- Use sysconfig.get_path() to get %%python3_sitelib and %%python3_sitearch
- Fixes: rhbz#1946972
* Mon Mar 29 2021 Miro Hrončok <mhroncok@redhat.com> - 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 <mhroncok@redhat.com> - 3.9-35
- Fix %%python_extras_subpkg with underscores in extras names
* Mon Feb 08 2021 Miro Hrončok <mhroncok@redhat.com> - 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 <mhroncok@redhat.com> - 3.9-13
- Automatically word-wrap the description of extras subpackages
- Fixes: rhbz#1922442
* Wed Jan 27 2021 Fedora Release Engineering <releng@fedoraproject.org> - 3.9-12
- Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild
* Tue Dec 08 2020 Miro Hrončok <mhroncok@redhat.com> - 3.9-11
- Support defining %%py3_shebang_flags to %%nil
* Mon Sep 14 2020 Miro Hrončok <mhroncok@redhat.com> - 3.9-10
* Thu Sep 24 2020 Miro Hrončok <mhroncok@redhat.com> - 3-59
- Add %%python3_platform_triplet and %%python3_ext_suffix
- https://fedoraproject.org/wiki/Changes/Python_Upstream_Architecture_Names
* Fri Jul 24 2020 Lumír Balhar <lbalhar@redhat.com> - 3.9-9
- Adapt %%py[3]_shebang_fix to use versioned pathfixX.Y.py
* Fri Jul 24 2020 Lumír Balhar <lbalhar@redhat.com> - 3.9-8
- Disable Python hash seed randomization in %%py_byte_compile
* Tue Jul 21 2020 Lumír Balhar <lbalhar@redhat.com> - 3.9-7
- Make %%py3_dist respect %%python3_pkgversion
* Thu Jul 16 2020 Miro Hrončok <mhroncok@redhat.com> - 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 <mhroncok@redhat.com> - 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 <lbalhar@redhat.com> - 3.9-4
- Use compileall from stdlib for Python >= 3.9
* Thu Jun 11 2020 Miro Hrončok <mhroncok@redhat.com> - 3.9-3
* Mon Jun 15 2020 Miro Hrončok <mhroncok@redhat.com> - 3-57
- Allow to combine %%pycached with other macros (e.g. %%exclude or %%ghost) (#1838992)
* Sat May 30 2020 Miro Hrončok <mhroncok@redhat.com> - 3.9-2
- Require the exact same version-release of other subpackages of this package
* Thu May 21 2020 Miro Hrončok <mhroncok@redhat.com> - 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 <mhroncok@redhat.com> - 3.8-8
* Wed May 20 2020 Miro Hrončok <mhroncok@redhat.com> - 3-56
- Implement %%py_provides
- Implement %%pytest
- Implement %%pyX_shebang_fix
- Strip tildes from %%version in %%pypi_source by default
* Thu May 07 2020 Miro Hrončok <mhroncok@redhat.com> - 3.8-7
- Change %%__default_python3_pkgversion from 38 to 3.8
* Tue May 05 2020 Miro Hrončok <mhroncok@redhat.com> - 3.8-6
- Require recent enough SRPM macros from RPM macros, to prevent missing Lua files
* Tue May 05 2020 Miro Hrončok <mhroncok@redhat.com> - 3.8-5
- Implement %%py_provides
* Mon May 04 2020 Tomas Hrnciar <thrnciar@redhat.com> - 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 <mhroncok@redhat.com> - 3.8-3
* Tue Apr 28 2020 Miro Hrončok <mhroncok@redhat.com> - 3-55
- Make pythonX-rpm-macros depend on python-rpm-macros (#1827811)
* Tue Mar 31 2020 Lumír Balhar <lbalhar@redhat.com> - 3.8-2
* Tue Mar 31 2020 Lumír Balhar <lbalhar@redhat.com> - 3-54
- Update of bundled compileall2 module to 0.7.1 (bugfix release)
* Mon Mar 23 2020 Miro Hrončok <mhroncok@redhat.com> - 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 <mhroncok@redhat.com> - 3-54
* Mon Feb 10 2020 Miro Hrončok <mhroncok@redhat.com> - 3-53
- 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 <mhroncok@redhat.com> - 3-53
* Fri Feb 07 2020 Miro Hrončok <mhroncok@redhat.com> - 3-52
- Define %%py(2|3)?_shbang_opts_nodash to be used with pathfix.py -a
* Thu Jan 30 2020 Fedora Release Engineering <releng@fedoraproject.org> - 3-52
- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild
* Sat Dec 28 2019 Miro Hrončok <mhroncok@redhat.com> - 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 <lbalhar@redhat.com> - 3-50
- Update of bundled compileall2 module
@ -504,7 +148,7 @@ Resolves: rhbz#2011056
- Add %%python_disable_dependency_generator
* Wed Dec 05 2018 Miro Hrončok <mhroncok@redhat.com> - 3-40
- Workaround leaking buildroot PATH in %%py_byte_compile (#1647212)
- Workaround leaking buildroot PATH in %py_byte_compile (#1647212)
* Thu Nov 01 2018 Petr Viktorin <pviktori@redhat.com> - 3-39
- Move "sleep 1" workaround from py3_build to py2_build (#1644923)

View file

@ -1,101 +0,0 @@
-- 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,
}

View file

@ -1,7 +0,0 @@
# completely disabled inspections:
inspections:
# there is no upstream and the files are changed from time to time
addedfiles: off
changedfiles: off
filesize: off
upstream: off

View file

@ -1,71 +0,0 @@
%global basedir /opt/test/byte_compilation
# We have 3 different ways of bytecompiling: for 3.9+, 3.4-3.8, and 2.7
# Test with a representative of each, except 2.7 which we no longer have
%global python36_sitelib /usr/lib/python3.6/site-packages
Name: pythontest
Version: 0
Release: 0%{?dist}
Summary: ...
License: MIT
BuildRequires: python3-devel
BuildRequires: python3.6
%description
...
%install
mkdir -p %{buildroot}%{basedir}/directory/to/test/recursion
echo "print()" > %{buildroot}%{basedir}/file.py
echo "print()" > %{buildroot}%{basedir}/directory/to/test/recursion/file_in_dir.py
%py_byte_compile %{python3} %{buildroot}%{basedir}/file.py
%py_byte_compile %{python3} %{buildroot}%{basedir}/directory
# Files in sitelib are compiled automatically by brp-python-bytecompile
mkdir -p %{buildroot}%{python3_sitelib}/directory/
echo "print()" > %{buildroot}%{python3_sitelib}/directory/file.py
mkdir -p %{buildroot}%{python36_sitelib}/directory/
echo "print()" > %{buildroot}%{python36_sitelib}/directory/file.py
%check
LOCATIONS="
%{buildroot}%{basedir}
%{buildroot}%{python3_sitelib}/directory/
%{buildroot}%{python36_sitelib}/directory/
"
# Count .py and .pyc files
PY=$(find $LOCATIONS -name "*.py" | wc -l)
PYC=$(find $LOCATIONS -name "*.py[co]" | wc -l)
# We should have 4 .py files (3 for python3, one for 3.6)
test $PY -eq 4
# Every .py file should be byte-compiled to two .pyc files (optimization level 0 and 1)
# so we should have two times more .pyc files than .py files
test $(expr $PY \* 2) -eq $PYC
# In this case the .pyc files should be identical across omtimization levels
# (they don't use docstrings and assert staements)
# So they should be hardlinked; the number of distinct inodes should match the
# number of source files. (Or be smaller, if the dupe detection is done
# across all files.)
INODES=$(stat --format %i $(find $LOCATIONS -name "*.py[co]") | sort -u | wc -l)
test $PY -ge $INODES
%files
%pycached %{basedir}/file.py
%pycached %{basedir}/directory/to/test/recursion/file_in_dir.py
%pycached %{python3_sitelib}/directory/file.py
%pycached %{python36_sitelib}/directory/file.py
%changelog
* Thu Jan 01 2015 Fedora Packager <nobody@fedoraproject.org> - 0-0
- This changelog entry exists and is deliberately set in the past

File diff suppressed because it is too large Load diff

View file

@ -1,472 +0,0 @@
from import_all_modules import argparser, exclude_unwanted_module_globs, import_modules
from import_all_modules import main as modules_main
from import_all_modules import read_modules_from_cli, filter_top_level_modules_only
from pathlib import Path
import pytest
import shlex
import sys
@pytest.fixture(autouse=True)
def preserve_sys_path():
original_sys_path = list(sys.path)
yield
sys.path = original_sys_path
@pytest.fixture(autouse=True)
def preserve_sys_modules():
original_sys_modules = dict(sys.modules)
yield
sys.modules = original_sys_modules
@pytest.mark.parametrize(
'args, imports',
[
('six', ['six']),
('five six seven', ['five', 'six', 'seven']),
('six,seven, eight', ['six', 'seven', 'eight']),
('six.quarter six.half,, SIX', ['six.quarter', 'six.half', 'SIX']),
('six.quarter six.half,, SIX \\ ', ['six.quarter', 'six.half', 'SIX']),
]
)
def test_read_modules_from_cli(args, imports):
argv = shlex.split(args)
cli_args = argparser().parse_args(argv)
assert read_modules_from_cli(cli_args.modules) == imports
@pytest.mark.parametrize(
'all_mods, imports',
[
(['six'], ['six']),
(['five', 'six', 'seven'], ['five', 'six', 'seven']),
(['six.seven', 'eight'], ['eight']),
(['SIX', 'six.quarter', 'six.half.and.sth', 'seven'], ['SIX', 'seven']),
],
)
def test_filter_top_level_modules_only(all_mods, imports):
assert filter_top_level_modules_only(all_mods) == imports
@pytest.mark.parametrize(
'globs, expected',
[
(['*.*'], ['foo', 'boo']),
(['?oo'], ['foo.bar', 'foo.bar.baz', 'foo.baz']),
(['*.baz'], ['foo', 'foo.bar', 'boo']),
(['foo'], ['foo.bar', 'foo.bar.baz', 'foo.baz', 'boo']),
(['foo*'], ['boo']),
(['foo*', '*bar'], ['boo']),
(['foo', 'bar'], ['foo.bar', 'foo.bar.baz', 'foo.baz', 'boo']),
(['*'], []),
]
)
def test_exclude_unwanted_module_globs(globs, expected):
my_modules = ['foo', 'foo.bar', 'foo.bar.baz', 'foo.baz', 'boo']
tested = exclude_unwanted_module_globs(globs, my_modules)
assert tested == expected
def test_cli_with_all_args():
'''A smoke test, all args must be parsed correctly.'''
mods = ['foo', 'foo.bar', 'baz']
files = ['-f', './foo']
top = ['-t']
exclude = ['-e', 'foo*']
cli_args = argparser().parse_args([*mods, *files, *top, *exclude])
assert cli_args.filename == [Path('foo')]
assert cli_args.top_level is True
assert cli_args.modules == ['foo', 'foo.bar', 'baz']
assert cli_args.exclude == ['foo*']
def test_cli_without_filename_toplevel():
'''Modules provided on command line (without files) must be parsed correctly.'''
mods = ['foo', 'foo.bar', 'baz']
cli_args = argparser().parse_args(mods)
assert cli_args.filename is None
assert cli_args.top_level is False
assert cli_args.modules == ['foo', 'foo.bar', 'baz']
def test_cli_with_filename_no_cli_mods():
'''Files (without any modules provided on command line) must be parsed correctly.'''
files = ['-f', './foo', '-f', './bar', '-f', './baz']
cli_args = argparser().parse_args(files)
assert cli_args.filename == [Path('foo'), Path('./bar'), Path('./baz')]
assert not cli_args.top_level
def test_main_raises_error_when_no_modules_provided():
'''If no filename nor modules were provided, ValueError is raised.'''
with pytest.raises(ValueError):
modules_main([])
def test_import_all_modules_does_not_import():
'''Ensure the files from /usr/lib/rpm/redhat cannot be imported and
checked for import'''
# We already imported it in this file once, make sure it's not imported
# from the cache
sys.modules.pop('import_all_modules')
with pytest.raises(SystemExit):
modules_main(['import_all_modules'])
def test_modules_from_cwd_not_found(tmp_path, monkeypatch):
test_module = tmp_path / 'this_is_a_module_in_cwd.py'
test_module.write_text('')
monkeypatch.chdir(tmp_path)
with pytest.raises(SystemExit):
modules_main(['this_is_a_module_in_cwd'])
def test_modules_from_sys_path_found(tmp_path):
test_module = tmp_path / 'this_is_a_module_in_sys_path.py'
test_module.write_text('')
sys.path.append(str(tmp_path))
modules_main(['this_is_a_module_in_sys_path'])
assert 'this_is_a_module_in_sys_path' in sys.modules
def test_modules_from_file_are_found(tmp_path):
test_file = tmp_path / 'this_is_a_file_in_tmp_path.txt'
test_file.write_text('math\nwave\ncsv\n')
# Make sure the tested modules are not already in sys.modules
for m in ('math', 'wave', 'csv'):
sys.modules.pop(m, None)
modules_main(['-f', str(test_file)])
assert 'csv' in sys.modules
assert 'math' in sys.modules
assert 'wave' in sys.modules
def test_modules_from_files_are_found(tmp_path):
test_file_1 = tmp_path / 'this_is_a_file_in_tmp_path_1.txt'
test_file_2 = tmp_path / 'this_is_a_file_in_tmp_path_2.txt'
test_file_3 = tmp_path / 'this_is_a_file_in_tmp_path_3.txt'
test_file_1.write_text('math\nwave\n')
test_file_2.write_text('csv\nnetrc\n')
test_file_3.write_text('logging\ncsv\n')
# Make sure the tested modules are not already in sys.modules
for m in ('math', 'wave', 'csv', 'netrc', 'logging'):
sys.modules.pop(m, None)
modules_main(['-f', str(test_file_1), '-f', str(test_file_2), '-f', str(test_file_3), ])
for module in ('csv', 'math', 'wave', 'netrc', 'logging'):
assert module in sys.modules
def test_nonexisting_modules_raise_exception_on_import(tmp_path):
test_file = tmp_path / 'this_is_a_file_in_tmp_path.txt'
test_file.write_text('nonexisting_module\nanother\n')
with pytest.raises(SystemExit):
modules_main(['-f', str(test_file)])
def test_nested_modules_found_when_expected(tmp_path, monkeypatch, capsys):
# This one is supposed to raise an error
cwd_path = tmp_path / 'test_cwd'
Path.mkdir(cwd_path)
test_module_1 = cwd_path / 'this_is_a_module_in_cwd.py'
# Nested structure that is supposed to be importable
nested_path_1 = tmp_path / 'nested'
nested_path_2 = nested_path_1 / 'more_nested'
for path in (nested_path_1, nested_path_2):
Path.mkdir(path)
test_module_2 = tmp_path / 'this_is_a_module_in_level_0.py'
test_module_3 = nested_path_1 / 'this_is_a_module_in_level_1.py'
test_module_4 = nested_path_2 / 'this_is_a_module_in_level_2.py'
for module in (test_module_1, test_module_2, test_module_3, test_module_4):
module.write_text('')
sys.path.append(str(tmp_path))
monkeypatch.chdir(cwd_path)
with pytest.raises(SystemExit):
modules_main([
'this_is_a_module_in_level_0',
'nested.this_is_a_module_in_level_1',
'nested.more_nested.this_is_a_module_in_level_2',
'this_is_a_module_in_cwd'])
_, err = capsys.readouterr()
assert 'Check import: this_is_a_module_in_level_0' in err
assert 'Check import: nested.this_is_a_module_in_level_1' in err
assert 'Check import: nested.more_nested.this_is_a_module_in_level_2' in err
assert 'Check import: this_is_a_module_in_cwd' in err
def test_modules_both_from_files_and_cli_are_imported(tmp_path):
test_file_1 = tmp_path / 'this_is_a_file_in_tmp_path_1.txt'
test_file_1.write_text('this_is_a_module_in_tmp_path_1')
test_file_2 = tmp_path / 'this_is_a_file_in_tmp_path_2.txt'
test_file_2.write_text('this_is_a_module_in_tmp_path_2')
test_module_1 = tmp_path / 'this_is_a_module_in_tmp_path_1.py'
test_module_2 = tmp_path / 'this_is_a_module_in_tmp_path_2.py'
test_module_3 = tmp_path / 'this_is_a_module_in_tmp_path_3.py'
for module in (test_module_1, test_module_2, test_module_3):
module.write_text('')
sys.path.append(str(tmp_path))
modules_main([
'-f', str(test_file_1),
'this_is_a_module_in_tmp_path_3',
'-f', str(test_file_2),
])
expected = (
'this_is_a_module_in_tmp_path_1',
'this_is_a_module_in_tmp_path_2',
'this_is_a_module_in_tmp_path_3',
)
for module in expected:
assert module in sys.modules
def test_non_existing_module_raises_exception(tmp_path):
test_module_1 = tmp_path / 'this_is_a_module_in_tmp_path_1.py'
test_module_1.write_text('')
sys.path.append(str(tmp_path))
with pytest.raises(SystemExit):
modules_main([
'this_is_a_module_in_tmp_path_1',
'this_is_a_module_in_tmp_path_2',
])
def test_import_module_returns_failed_modules(tmp_path):
test_module_1 = tmp_path / 'this_is_a_module_in_tmp_path_1.py'
test_module_1.write_text('')
sys.path.append(str(tmp_path))
failed_modules = import_modules([
'this_is_a_module_in_tmp_path_1',
'this_is_a_module_in_tmp_path_2',
])
assert failed_modules == ['this_is_a_module_in_tmp_path_2']
def test_module_with_error_propagates_exception(tmp_path, capsys):
test_module_1 = tmp_path / 'this_is_a_module_in_tmp_path_1.py'
test_module_1.write_text('0/0')
sys.path.append(str(tmp_path))
with pytest.raises(SystemExit):
modules_main([
'this_is_a_module_in_tmp_path_1',
])
_, err = capsys.readouterr()
assert "ZeroDivisionError" in err
def test_import_module_returns_empty_list_when_no_modules_failed(tmp_path):
test_module_1 = tmp_path / 'this_is_a_module_in_tmp_path_1.py'
test_module_1.write_text('')
sys.path.append(str(tmp_path))
failed_modules = import_modules(['this_is_a_module_in_tmp_path_1'])
assert failed_modules == []
def test_all_modules_are_imported(tmp_path, capsys):
test_module_1 = tmp_path / 'this_is_a_module_in_tmp_path_1.py'
test_module_2 = tmp_path / 'this_is_a_module_in_tmp_path_2.py'
test_module_3 = tmp_path / 'this_is_a_module_in_tmp_path_3.py'
for module in (test_module_1, test_module_2, test_module_3):
module.write_text('')
sys.path.append(str(tmp_path))
with pytest.raises(SystemExit):
modules_main([
'this_is_a_module_in_tmp_path_1',
'missing_module',
'this_is_a_module_in_tmp_path_2',
'this_is_a_module_in_tmp_path_3',
])
_, err = capsys.readouterr()
for i in range(1, 4):
assert f"Check import: this_is_a_module_in_tmp_path_{i}" in err
assert "Failed to import: missing_module" in err
def test_correct_modules_are_excluded(tmp_path):
test_module_1 = tmp_path / 'module_in_tmp_path_1.py'
test_module_2 = tmp_path / 'module_in_tmp_path_2.py'
test_module_3 = tmp_path / 'module_in_tmp_path_3.py'
for module in (test_module_1, test_module_2, test_module_3):
module.write_text('')
sys.path.append(str(tmp_path))
test_file_1 = tmp_path / 'a_file_in_tmp_path_1.txt'
test_file_1.write_text('module_in_tmp_path_1\nmodule_in_tmp_path_2\nmodule_in_tmp_path_3\n')
modules_main([
'-e', 'module_in_tmp_path_2',
'-f', str(test_file_1),
'-e', 'module_in_tmp_path_3',
])
assert 'module_in_tmp_path_1' in sys.modules
assert 'module_in_tmp_path_2' not in sys.modules
assert 'module_in_tmp_path_3' not in sys.modules
def test_excluding_all_modules_raises_error(tmp_path):
test_module_1 = tmp_path / 'module_in_tmp_path_1.py'
test_module_2 = tmp_path / 'module_in_tmp_path_2.py'
test_module_3 = tmp_path / 'module_in_tmp_path_3.py'
for module in (test_module_1, test_module_2, test_module_3):
module.write_text('')
sys.path.append(str(tmp_path))
test_file_1 = tmp_path / 'a_file_in_tmp_path_1.txt'
test_file_1.write_text('module_in_tmp_path_1\nmodule_in_tmp_path_2\nmodule_in_tmp_path_3\n')
with pytest.raises(ValueError):
modules_main([
'-e', 'module_in_tmp_path*',
'-f', str(test_file_1),
])
def test_only_toplevel_modules_found(tmp_path):
# Nested structure that is supposed to be importable
nested_path_1 = tmp_path / 'nested'
nested_path_2 = nested_path_1 / 'more_nested'
for path in (nested_path_1, nested_path_2):
Path.mkdir(path)
test_module_1 = tmp_path / 'this_is_a_module_in_level_0.py'
test_module_2 = nested_path_1 / 'this_is_a_module_in_level_1.py'
test_module_3 = nested_path_2 / 'this_is_a_module_in_level_2.py'
for module in (test_module_1, test_module_2, test_module_3):
module.write_text('')
sys.path.append(str(tmp_path))
modules_main([
'this_is_a_module_in_level_0',
'nested.this_is_a_module_in_level_1',
'nested.more_nested.this_is_a_module_in_level_2',
'-t'])
assert 'nested.this_is_a_module_in_level_1' not in sys.modules
assert 'nested.more_nested.this_is_a_module_in_level_2' not in sys.modules
def test_only_toplevel_included_modules_found(tmp_path):
# Nested structure that is supposed to be importable
nested_path_1 = tmp_path / 'nested'
nested_path_2 = nested_path_1 / 'more_nested'
for path in (nested_path_1, nested_path_2):
Path.mkdir(path)
test_module_1 = tmp_path / 'this_is_a_module_in_level_0.py'
test_module_4 = tmp_path / 'this_is_another_module_in_level_0.py'
test_module_2 = nested_path_1 / 'this_is_a_module_in_level_1.py'
test_module_3 = nested_path_2 / 'this_is_a_module_in_level_2.py'
for module in (test_module_1, test_module_2, test_module_3, test_module_4):
module.write_text('')
sys.path.append(str(tmp_path))
modules_main([
'this_is_a_module_in_level_0',
'this_is_another_module_in_level_0',
'nested.this_is_a_module_in_level_1',
'nested.more_nested.this_is_a_module_in_level_2',
'-t',
'-e', '*another*'
])
assert 'nested.this_is_a_module_in_level_1' not in sys.modules
assert 'nested.more_nested.this_is_a_module_in_level_2' not in sys.modules
assert 'this_is_another_module_in_level_0' not in sys.modules
assert 'this_is_a_module_in_level_0' in sys.modules
def test_module_list_from_relative_path(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
test_file_1 = Path('this_is_a_file_in_cwd.txt')
test_file_1.write_text('wave')
sys.modules.pop('wave', None)
modules_main([
'-f', 'this_is_a_file_in_cwd.txt'
])
assert 'wave' in sys.modules
@pytest.mark.parametrize('arch_in_path', [True, False])
def test_pth_files_are_read_from__PYTHONSITE(arch_in_path, tmp_path, monkeypatch, capsys):
sitearch = tmp_path / 'lib64'
sitearch.mkdir()
sitelib = tmp_path / 'lib'
sitelib.mkdir()
for where, word in (sitearch, "ARCH"), (sitelib, "LIB"), (sitelib, "MOD"):
module = where / f'print{word}.py'
module.write_text(f'print("{word}")')
pth_sitearch = sitearch / 'ARCH.pth'
pth_sitearch.write_text('import printARCH\n')
pth_sitelib = sitelib / 'LIB.pth'
pth_sitelib.write_text('import printLIB\n')
if arch_in_path:
sys.path.append(str(sitearch))
sys.path.append(str(sitelib))
# we always add sitearch to _PYTHONSITE
# but when not in sys.path, it should not be processed for .pth files
monkeypatch.setenv('_PYTHONSITE', f'{sitearch}:{sitelib}')
modules_main(['printMOD'])
out, err = capsys.readouterr()
if arch_in_path:
assert out == 'ARCH\nLIB\nMOD\n'
else:
assert out == 'LIB\nMOD\n'

View file

@ -1,41 +0,0 @@
from pathlib import Path
import os
import pytest
import subprocess
@pytest.fixture
def create_test_files(tmp_path):
def _create(subpath, installer_content):
dir_path = tmp_path / subpath
dir_path.mkdir(parents=True, exist_ok=True)
installer_file = dir_path / "INSTALLER"
installer_file.write_text(installer_content)
record_file = dir_path / "RECORD"
record_file.write_text("dummy content in RECORD file\n")
return dir_path
return _create
testdata = [
("usr/lib/python3.13/site-packages/zipp-3.19.2.dist-info/", "pip\n", "rpm\n", False),
("usr/lib64/python3.13/site-packages/zipp-3.19.2.dist-info/", "pip\n", "rpm\n", False),
("usr/lib/python3.13/site-packages/setuptools/_vendor/zipp-3.19.2.dist-info/", "pip\n", "pip\n", True),
("usr/lib64/python3.13/site-packages/setuptools/_vendor/zipp-3.19.2.dist-info/", "pip\n", "pip\n", True),
("usr/lib/python3.13/site-packages/zipp-3.19.2.dist-info/","not pip in INSTALLER\n", "not pip in INSTALLER\n", True),
("usr/lib64/python3.13/site-packages/zipp-3.19.2.dist-info/","not pip in INSTALLER\n", "not pip in INSTALLER\n", True),
]
@pytest.mark.parametrize("path, installer_content, expected_installer_content, record_file_exists", testdata)
def test_installer_file_was_correctly_modified(monkeypatch, create_test_files,
path, installer_content, expected_installer_content, record_file_exists):
script_path = Path("/usr/lib/rpm/redhat/brp-python-rpm-in-distinfo")
tmp_dir = create_test_files(path, installer_content)
monkeypatch.setenv("RPM_BUILD_ROOT", str(tmp_dir))
result = subprocess.run(
[script_path],
capture_output=True, text=True
)
assert result.returncode == 0
assert (Path(tmp_dir) / "INSTALLER").read_text() == expected_installer_content
assert Path(tmp_dir / "RECORD").exists() is record_file_exists

23
tests/tests.yml Normal file
View file

@ -0,0 +1,23 @@
---
- hosts: localhost
tags:
- classic
tasks:
- dnf:
name: "*"
state: latest
- hosts: localhost
roles:
- role: standard-test-basic
tags:
- classic
tests:
- pytest:
dir: .
run: pytest -v
required_packages:
- rpm-build
- python-rpm-macros
- python3-rpm-macros
- python3-pytest

View file

@ -1,116 +0,0 @@
Name: testwheel
Epoch: 42
Version: 1
Release: 0%{?dist}
Summary: ...
License: MIT
BuildArch: noarch
BuildRequires: python3-devel
BuildRequires: python3-setuptools >= 61
BuildRequires: python3-pip
%description
This builds and installs a wheel which we can then use as a test for
%%python_wheel_inject_sbom.
%prep
cat > pyproject.toml << EOF
[project]
name = "testwheel"
version = "1"
[build-system]
requires = ["setuptools >= 61"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
include-package-data = true
[tool.setuptools.packages.find]
include = ["testwheel*"]
EOF
# create a secondary dist-info folder in the project
# we need to ensure this file is not altered
mkdir -p testwheel/_vendor/dependency-2.2.2.dist-info
touch testwheel/_vendor/dependency-2.2.2.dist-info/RECORD
echo 'recursive-include testwheel/_vendor *' > MANIFEST.in
%build
export PIP_CONFIG_FILE=/dev/null
%{python3} -m pip wheel . --no-build-isolation
# The macro should happily alter multiple wheels, let's make more
for i in {1..5}; do
mkdir ${i}
cp -a *.whl ${i}
done
# using relative paths should succeed
%python_wheel_inject_sbom {1..5}/*.whl
# repetitive use should bail out and fail (SBOM is already there)
%{python_wheel_inject_sbom {1..5}/*.whl} && exit 1 || true
# each wheel should already have it, all should fail individually as well
for i in {1..5}; do
%{python_wheel_inject_sbom ${i}/*.whl} && exit 1 || true
done
%install
mkdir -p %{buildroot}%{python_wheel_dir}
cp -a *.whl %{buildroot}%{python_wheel_dir}
# using absolute paths should work
%python_wheel_inject_sbom %{buildroot}%{python_wheel_dir}/*.whl
# and fail when repeated
%{python_wheel_inject_sbom %{buildroot}%{python_wheel_dir}/*.whl} && exit 1 || true
%check
%define venvsite venv/lib/python%{python3_version}/site-packages
%{python3} -m venv venv
venv/bin/pip install --no-index --no-cache-dir %{buildroot}%{python_wheel_dir}/*.whl
test -f %{venvsite}/testwheel-1.dist-info/RECORD
test -f %{venvsite}/testwheel-1.dist-info/sboms/bom.json
grep '^testwheel-1.dist-info/sboms/bom.json,' %{venvsite}/testwheel-1.dist-info/RECORD
# a more specific grep. we don't care about CRLF line ends (pip uses those? without the sed the $ doesn't match line end)
sed 's/\r//g' %{venvsite}/testwheel-1.dist-info/RECORD | grep -E '^testwheel-1.dist-info/sboms/bom.json,sha256=[a-f0-9]{64},[0-9]+$'
test -f %{venvsite}/testwheel/_vendor/dependency-2.2.2.dist-info/RECORD
test -f %{venvsite}/testwheel/_vendor/dependency-2.2.2.dist-info/sboms/bom.json && exit 1 || true
# this deliberately uses a different mechanism than the macro
# if you are running this test on a different distro, adjust it
%define ns %{?fedora:fedora}%{?eln:fedora}%{?epel:epel}%{!?eln:%{!?epel:%{?rhel:redhat}}}
PYTHONOPTIMIZE=0 %{python3} -c "
import json
with open('%{venvsite}/testwheel-1.dist-info/sboms/bom.json') as fp:
sbom = json.load(fp)
assert len(sbom['components']) == 1
assert sbom['components'][0]['type'] == 'library'
assert sbom['components'][0]['name'] == 'testwheel'
assert sbom['components'][0]['version'] == '1-0%{?dist}'
assert sbom['components'][0]['purl'] == 'pkg:rpm/%{ns}/testwheel@1-0%{?dist}?epoch=42&arch=src'
"
# replace the installation with the original unaltered wheel
venv/bin/pip install --force-reinstall --no-index --no-cache-dir *.whl
test -f %{venvsite}/testwheel-1.dist-info/RECORD
# no SBOM
test ! -e %{venvsite}/testwheel-1.dist-info/sboms/bom.json
grep '^testwheel-1.dist-info/sboms/bom.json,' %{venvsite}/testwheel-1.dist-info/RECORD && exit 1 || true
%files
%{python_wheel_dir}/*.whl
%changelog
* Wed Aug 13 2025 Miro Hrončok <mhroncok@redhat.com> - 42:1-0
- A static changelog with a date, so we can clamp mtimes