diff --git a/epel-rpm-macros.spec b/epel-rpm-macros.spec index 5757d66..bc2e324 100644 --- a/epel-rpm-macros.spec +++ b/epel-rpm-macros.spec @@ -1,9 +1,10 @@ Name: epel-rpm-macros Version: 8 -Release: 39 +Release: 40 Summary: Extra Packages for Enterprise Linux RPM macros -License: GPLv2 +# import_all_modules.py: MIT +License: GPLv2 and MIT # This is a EPEL maintained package which is specific to # our distribution. Thus the source is only available from @@ -27,6 +28,9 @@ Source151: https://src.fedoraproject.org/rpms/redhat-rpm-config/raw/rawhide %global rpmautospec_commit 52f3c2017e10c5ab5a183fed772e9fe8a86a20fb Source152: https://pagure.io/fedora-infra/rpmautospec/raw/%{rpmautospec_commit}/f/rpm/macros.d/macros.rpmautospec +# Python code +Source302: import_all_modules.py + BuildArch: noarch Requires: redhat-release >= %{version} # For FPC buildroot macros @@ -106,6 +110,10 @@ install -Dpm 644 %{SOURCE151} \ install -Dpm 644 %{SOURCE152} \ %{buildroot}%{_rpmmacrodir}/macros.rpmautospec +# python scripts +mkdir -p %{buildroot}%{_rpmconfigdir}/redhat +install -Dpm 644 %{SOURCE302} %{buildroot}%{_rpmconfigdir}/redhat/ + %files %license GPL %{_rpmmacrodir}/macros.epel-rpm-macros @@ -117,6 +125,9 @@ install -Dpm 644 %{SOURCE152} \ %{_rpmmacrodir}/macros.build-constraints %{_rpmmacrodir}/macros.shell-completions +# python scripts +%{_rpmconfigdir}/redhat/import_all_modules.py + %files systemd # sysusers %{_rpmconfigdir}/macros.d/macros.sysusers @@ -126,6 +137,9 @@ install -Dpm 644 %{SOURCE152} \ %changelog +* Fri Oct 06 2023 Orion Poplawski - 8-40 +- Add full %%py3_check_import macro + * Fri Apr 07 2023 Miro HronĨok - 8-39 - Prepare support for Python 3.11 diff --git a/import_all_modules.py b/import_all_modules.py new file mode 100644 index 0000000..3930236 --- /dev/null +++ b/import_all_modules.py @@ -0,0 +1,171 @@ +'''Script to perform import of each module given to %%py_check_import +''' +import argparse +import importlib +import fnmatch +import os +import re +import site +import sys + +from contextlib import contextmanager +from pathlib import Path + + +def read_modules_files(file_paths): + '''Read module names from the files (modules must be newline separated). + + Return the module names list or, if no files were provided, an empty list. + ''' + + if not file_paths: + return [] + + modules = [] + for file in file_paths: + file_contents = file.read_text() + modules.extend(file_contents.split()) + return modules + + +def read_modules_from_cli(argv): + '''Read module names from command-line arguments (space or comma separated). + + Return the module names list. + ''' + + if not argv: + return [] + + # %%py3_check_import allows to separate module list with comma or whitespace, + # we need to unify the output to a list of particular elements + modules_as_str = ' '.join(argv) + modules = re.split(r'[\s,]+', modules_as_str) + # Because of shell expansion in some less typical cases it may happen + # that a trailing space will occur at the end of the list. + # Remove the empty items from the list before passing it further + modules = [m for m in modules if m] + return modules + + +def filter_top_level_modules_only(modules): + '''Filter out entries with nested modules (containing dot) ie. 'foo.bar'. + + Return the list of top-level modules. + ''' + + return [module for module in modules if '.' not in module] + + +def any_match(text, globs): + '''Return True if any of given globs fnmatchcase's the given text.''' + + return any(fnmatch.fnmatchcase(text, g) for g in globs) + + +def exclude_unwanted_module_globs(globs, modules): + '''Filter out entries which match the either of the globs given as argv. + + Return the list of filtered modules. + ''' + + return [m for m in modules if not any_match(m, globs)] + + +def read_modules_from_all_args(args): + '''Return a joined list of modules from all given command-line arguments. + ''' + + modules = read_modules_files(args.filename) + modules.extend(read_modules_from_cli(args.modules)) + if args.exclude: + modules = exclude_unwanted_module_globs(args.exclude, modules) + + if args.top_level: + modules = filter_top_level_modules_only(modules) + + # Error when someone accidentally managed to filter out everything + if len(modules) == 0: + raise ValueError('No modules to check were left') + + return modules + + +def import_modules(modules): + '''Procedure to perform import check for each module name from the given list of modules. + ''' + + for module in modules: + print('Check import:', module, file=sys.stderr) + importlib.import_module(module) + + +def argparser(): + parser = argparse.ArgumentParser( + description='Generate list of all importable modules for import check.' + ) + parser.add_argument( + 'modules', nargs='*', + help=('Add modules to check the import (space or comma separated).'), + ) + parser.add_argument( + '-f', '--filename', action='append', type=Path, + help='Add importable module names list from file.', + ) + parser.add_argument( + '-t', '--top-level', action='store_true', + help='Check only top-level modules.', + ) + parser.add_argument( + '-e', '--exclude', action='append', + help='Provide modules globs to be excluded from the check.', + ) + return parser + + +@contextmanager +def remove_unwanteds_from_sys_path(): + '''Remove cwd and this script's parent from sys.path for the import test. + Bring the original contents back after import is done (or failed) + ''' + + cwd_absolute = Path.cwd().absolute() + this_file_parent = Path(__file__).parent.absolute() + old_sys_path = list(sys.path) + for path in old_sys_path: + if Path(path).absolute() in (cwd_absolute, this_file_parent): + sys.path.remove(path) + try: + yield + finally: + sys.path = old_sys_path + + +def addsitedirs_from_environ(): + '''Load directories from the _PYTHONSITE environment variable (separated by :) + and load the ones already present in sys.path via site.addsitedir() + to handle .pth files in them. + + This is needed to properly import old-style namespace packages with nspkg.pth files. + See https://bugzilla.redhat.com/2018551 for a more detailed rationale.''' + for path in os.getenv('_PYTHONSITE', '').split(':'): + if path in sys.path: + site.addsitedir(path) + + +def main(argv=None): + + cli_args = argparser().parse_args(argv) + + if not cli_args.modules and not cli_args.filename: + raise ValueError('No modules to check were provided') + + modules = read_modules_from_all_args(cli_args) + + with remove_unwanteds_from_sys_path(): + addsitedirs_from_environ() + import_modules(modules) + + +if __name__ == '__main__': + main() diff --git a/macros.epel-rpm-macros b/macros.epel-rpm-macros index 9f5333b..3d51370 100644 --- a/macros.epel-rpm-macros +++ b/macros.epel-rpm-macros @@ -52,16 +52,29 @@ %{__python2} -c "import %{lua:local m=rpm.expand('%{?*}'):gsub('[%s,]+', ', ');print(m)}" ) } +# 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:\\\ - %{-e:echo 'WARNING: The -e option of %%%%py3_check_import is not currently supported on EPEL.' >&2} - %{-t:echo 'WARNING: The -t option of %%%%py3_check_import is not currently supported on EPEL.' >&2} - %{-f:echo 'WARNING: The -f option of %%%%py3_check_import is not currently supported on EPEL.' >&2} - (cd %{_topdir} &&\\\ PATH="%{buildroot}%{_bindir}:$PATH"\\\ PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}}"\\\ + _PYTHONSITE="%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}"\\\ PYTHONDONTWRITEBYTECODE=1\\\ - %{__python3} -c "import %{lua:local m=rpm.expand('%{?*}'):gsub('[%s,]+', ', ');print(m)}" - ) + %{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) + } } # When packagers go against the Packaging Guidelines and disable the runtime