Compare commits

...

145 Commits
f28 ... rawhide

Author SHA1 Message Date
Fedora Release Engineering 8847b3750a Rebuilt for https://fedoraproject.org/wiki/Fedora_37_Mass_Rebuild
Signed-off-by: Fedora Release Engineering <releng@fedoraproject.org>
2022-07-22 22:07:20 +00:00
Miro Hrončok 4abed5f105 Use the values of %_py3_shebang_s and %_py3_shebang_P in the shebang opts/flags
As proposed in https://src.fedoraproject.org/rpms/python-rpm-macros/pull-request/141#comment-109228
And discussed in:

 - https://lists.fedoraproject.org/archives/list/python-devel@lists.fedoraproject.org/thread/4YD2X7HU5U5DFO3N4FWJLPSKVMKH4VSB/
 - https://lists.fedoraproject.org/archives/list/packaging@lists.fedoraproject.org/thread/4YD2X7HU5U5DFO3N4FWJLPSKVMKH4VSB/
2022-07-19 16:30:33 +02:00
Miro Hrončok 4d31ea8034 https://fedoraproject.org/wiki/Changes/PythonSafePath 2022-07-19 16:30:32 +02:00
Miro Hrončok 4085ef49f2 Define %python3_cache_tag / %python_cache_tag, e.g. cpython-311
When reviewing https://src.fedoraproject.org/rpms/pyproject-rpm-macros/pull-request/291
we have discovered that there is no macronized way to get this part of some paths
and that packagers need to hardcode it as cpython-%{python3_version_nodots}.

This way, we have a standardized macro packagers (and other macros) can use.
2022-07-19 11:42:56 +02:00
Tomáš Hrnčiar b8b5cb92da Python 3.11
https://fedoraproject.org/wiki/Changes/Python3.11
2022-06-13 11:23:37 +02:00
Miro Hrončok d174f03f62 Merge f36 and rawhide
A change was accidentaly merged to f36 only
2022-06-09 13:48:28 +02:00
Owen W. Taylor 546e9a3544 Support installing to %{_prefix} other than /usr
Pass %{_prefix} to install commands and when determining
the sitelib and sitearch variables.

https://lists.fedoraproject.org/archives/list/python-devel@lists.fedoraproject.org/thread/KEQMMNJ4HTTHSQLK6P4DJJTVPA36SS3W/

Co-Authored-By: Miro Hrončok <miro@hroncok.cz>
2022-06-08 13:03:06 +02:00
Miro Hrončok cfa45dfdf3 Add a note: Python 3.11+ no longer needs PYTHONHASHSEED=0
Implemented in:  https://github.com/python/cpython/pull/27926

We keep using it thou, because this is Python version agnostic.

Once we drop support for anything older than 3.11, we can remove it.
That'll be around ~2030. Assuming the world still exists by then.
2022-05-12 11:43:00 +02:00
Miro Hrončok 9102e29afc Don't use `! ...` as a check
See https://lists.fedoraproject.org/archives/list/packaging@lists.fedoraproject.org/thread/TFQGD7CSTD5WVKVT3WDIGF5D6DID5NK6/
2022-04-06 19:08:32 +02:00
Tomas Orsava e250f28d09 %py_provides: Do not generate Obsoletes for names containing parentheses
This mechanism is already implemented in the old %python_provide macro.
2022-02-08 12:39:49 +01:00
Miro Hrončok 99e7d8694c Explicitly opt-out from Python name-based provides and obsoletes generators
In Koji, python3-rpm-generators are not installed during the build.
However, packagers can have them installed locally, in mock or in Copr.
This way, we make sure the automatic provides (and obsoletes)
do not magically appear only in some environments.

Since python3-rpm-macros actually requires python-rpm-macros,
the requirement is self-satisfied when the automatic provides are generated.
2022-01-31 12:05:30 +01:00
Charalampos Stratakis 2ebee9d4cb Disable certain rpminspect inspections not relevant to this package 2022-01-28 17:10:16 +01:00
Miro Hrončok 5547a87f0b Add eval tests to RHEL %py_provides Obsoletes functionality 2022-01-20 18:57:55 +01:00
Tomas Orsava 5d7727c2aa Add lua helper functions to make it possible to automatically generate Obsoletes tags 2022-01-20 18:57:25 +01:00
Miro Hrončok a8b26546eb Set %__python3 value according to %python3_pkgversion
I.e. when %python3_pkgversion is 3.12, %__python3 is /usr/bin/python3.12

We assume that when packagers pacakge for Python 3.X, they want to change both
%python3_pkgversion and %__python3 value.

Hence instead of copy-pasting this:

    %global python3_pkgversion 3.X
    %global __python3 /usr/bin/python3.X

They just need to do:

    %global python3_pkgversion 3.X

Packagers who want to change the value of %__python3 without touching
%python3_pkgversion can still do it:

    %global __python3 /usr/bin/pypy3

Related to https://bugzilla.redhat.com/1821489
2021-12-08 15:35:51 +01:00
Miro Hrončok b55e6151bd Move %python3_pkgversion definition earlier in the file
So we can use it later in %__python3 and maintain readability.
2021-12-08 14:37:09 +01:00
Miro Hrončok 9eae0ccaf1 Move import_all_modules out of python-srpm-macros
There's no need for it in the default buildroot.
2021-11-02 16:05:11 +01:00
Miro Hrončok 9d81ad40e7 %py(3)_check_import: Process .pth files in site(arch|lib)
Fixes https://bugzilla.redhat.com/show_bug.cgi?id=2018551
2021-11-02 16:05:11 +01:00
Karolina Surma 824ef3d4af Fix %%py_shebang_flags handling within %%py_check_import
%%py{3}_check_import now respects the custom setting of %%py{3}_shebang_flags
and invokes Python with the respective values.
If %%py{3}_shebang_flags is undefined or set to no value,
there no flags are passed to Python on invoke.
Resolves: rhbz#2018615
2021-11-02 16:05:09 +01:00
Karolina Surma b20d8aa23a Allow multiline arguments processing for %%py3_check_import
Fixes the regression introduced with the macro reimplementation.
Resolves: rhbz#2018809
2021-11-02 16:03:14 +01:00
Karolina Surma 2d0673afb1 Add new options for %%py{3}_check_import: -f, -t, -e
-f: optionally read a file with module names to test
-t: bool flag - if set, filter only top-level modules
-e: optionally exclude module names matching the given glob (Unix
shell-style wildcards)
Importing all modules may cause bogus failures in some cases,
eg. when the imported code assumes there is an existing graphical window.
Such behaviour may be by design, hence for automatic processing it's
more convinient to - in some cases - check only for top-level modules
or filter out the troublemakers.
2021-10-27 15:57:37 +02:00
Tomas Orsava 9b797df44d Define a new macros %python_wheel_dir and %python_wheel_pkg_prefix 2021-10-20 16:40:57 +02:00
Lumir Balhar 7b546cae36 Non-existing path in py_reproducible_pyc_path causes build to fail 2021-10-12 15:50:50 +02:00
Miro Hrončok 5b578a851f Set $RPM_BUILD_ROOT in %{python3_...} macros, for alternate sysconfig install scheme
Our Pythons currently patches distutils to install packages to
/usr/lib(64)/pythonX.Y/site-packages when the $RPM_BUILD_ROOT environment
variable is set (and to /usr/local/lib(64)/pythonX.Y/site-packages otherwise).
With the deprecation of distutils [1] we want to change the patch to create
and use a different sysconfig install scheme [2].

However, we have realized that macros defined as %(%{__python3} ...) don't
"see" the environment variables set by rpmbuild because they are expanded earlier
and hence e.g. %{python3_sitelib} evaluates to
/usr/local/lib/python3.X/site-packages -- which is not desired.
To be able to reliably detect an RPM build environment by checking
the presence of the $RPM_BUILD_ROOT environment variable,
we manually set it in the macro definitions.

Since %{buildroot} in not fully populated
(e.g. it can expand literally to
/home/anna/rpmbuild/BUILDROOT/%{NAME}-%{VERSION}-%{RELEASE}.x86_64),
we don't use it here.
The variable simply needs to present in the environment.

See also the analysis of the build failures when this is not done [3].

[1] https://www.python.org/dev/peps/pep-0632/
[2] https://bugs.python.org/issue43976
[3] https://src.fedoraproject.org/rpms/python3.10/pull-request/63#comment-79042
2021-09-17 16:56:20 +02:00
Petr Viktorin 77cc1a43a2 Test bytecompilation & hardlinking with 3.8 and 2.7 2021-09-10 17:50:49 +02:00
Petr Viktorin 0044db1e8a Remove unneeded arguments from the python_bytecompile function
- For depth, use sys.getrecursionlimit()
  - the default from bytecompile2
  - we'll get a RecursionError before it's exceeded, anyway
- Set real_libdir locally
2021-09-10 17:50:49 +02:00
Petr Viktorin dd8caa5aa3 Use --hardlink-dupes for Python 3.4+ as well 2021-09-10 16:14:28 +02:00
Petr Viktorin 37bf640f37 Use --hardlink-dupes in %py_byte_compile and brp-python-bytecompile
(for Python 3.9+)

Resolves: rhbz#1977895
2021-09-10 10:50:01 +02:00
Miro Hrončok 76209d7bf3 Fedora CI eval tests: Make the python3.6 dependency optional
It is not available on RHEL 9, where we would like to be able to run the tests.

See https://bugzilla.redhat.com/show_bug.cgi?id=1984407
2021-08-24 21:35:32 +02:00
Fedora Release Engineering b1488aa40c - Rebuilt for https://fedoraproject.org/wiki/Fedora_35_Mass_Rebuild
Signed-off-by: Fedora Release Engineering <releng@fedoraproject.org>
2021-07-23 09:18:36 +00:00
Miro Hrončok fd3dc4c5dc Move Python related BuildRoot Policy scripts from redhat-rpm-config to python-srpm-macros
This allows us to maintain our own BuildRoot Policy scripts in an easier way.

This change needs to be coordinated with the removal of the files from redhat-rpm-config.

redhat-rpm-config requires python-srpm-macros, so no change is expected for the packagers.
2021-07-08 12:58:05 +02:00
Miro Hrončok c2305ea368 Introduce %py3_check_import
With $PATH and $PYTHONPATH set to the %buildroot,
the macro tries to import the given Python 3 module(s).
Useful as a smoke test in %check when ruining tests is not feasible.
Accepts spaces or commas as separators.

Package python-six:

    %check
    %py3_check_import six

    Executing(%check): ...
    ...
    + PATH=...
    + PYTHONPATH=...
    + PYTHONDONTWRITEBYTECODE=1
    + /usr/bin/python3 -c 'import six'
    + RPM_EC=0
    ++ jobs -p
    + exit 0

    %py3_check_import six seven

    ...
    + /usr/bin/python3 -c 'import six, seven'
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
    ModuleNotFoundError: No module named 'seven'

    error: Bad exit status from ... (%check)

    ...
    %py3_check_import five, six, seven

    + /usr/bin/python3 -c 'import five, six, seven'
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
    ModuleNotFoundError: No module named 'five'

    error: Bad exit status from ... (%check)

Package python-packaging:

    %py3_check_import packaging, packaging.markers  packaging.requirements, packaging.tags

    Executing(%check): ...
    ...
    + PATH=...
    + PYTHONPATH=...
    + PYTHONDONTWRITEBYTECODE=1
    + /usr/bin/python3 -c 'import packaging, packaging.markers, packaging.requirements, packaging.tags'
    + RPM_EC=0
    ++ jobs -p
    + exit 0

    %py3_check_import packaging, packaging.markers  packaging.notachance, packaging.tags

    ...
    + /usr/bin/python3 -c 'import packaging, packaging.markers, packaging.notachance, packaging.tags'
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
    ModuleNotFoundError: No module named 'packaging.notachance'

    error: Bad exit status from ... (%check)
2021-07-07 14:46:04 +02:00
Miro Hrončok bc2b51d6d3 Include brp-python-hardlink in python-srpm-macros since it is no longer in RPM 4.17+
See def9a339d2
2021-06-30 18:13:53 +02:00
Miro Hrončok d905710a8d %pytest: Set $PYTEST_ADDOPTS when %{__pytest_addopts} is defined
Related to https://bugzilla.redhat.com/show_bug.cgi?id=1935212
2021-06-28 11:34:10 +02:00
Ben Burton c487f82ef7 Adapt macros and BRP scripts for %topdir with spaces
Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1947416
2021-06-28 11:33:07 +02:00
Miro Hrončok 9dff7fbf6a Fix %python_provide when fed python3.10-foo to obsolete python-foo instead of python--foo
This has unlikely broken anything in practice,
no packages in Fedora use %python_provide with major.minor-version-prefixed names.
2021-06-15 16:15:45 +02:00
Petr Viktorin 370b825e45 Add the project's canonical URL 2021-06-03 11:44:21 +02:00
Miro Hrončok 3a654e3bed Python 3.10
https://fedoraproject.org/wiki/Changes/Python3.10
2021-06-01 15:58:40 +02:00
Miro Hrončok 2b43f896af Update %python3_pkgversion comment 2021-04-27 16:04:58 +02:00
Miro Hrončok 14e4c04a42 Remove EPEL 7 compatibility macros that were actually not defined at all
The %{? in the comment made the entire block of macros not work.

Since nobody actually used those on Fedora, because they did not exist,
we can safely remove them. No need to document this in the %changelog.
2021-04-27 15:49:21 +02:00
Miro Hrončok 03a1e3ba65 Escape % symbols in macro files comments
This is most likely not neccessary but can prevent serious problems like:

https://bugzilla.redhat.com/show_bug.cgi?id=1953910
2021-04-27 12:36:11 +02:00
Miro Hrončok cad73c2159 Be more careful when loading the macros from sources
The %{?load:%{SOURCE102}} construct no longer works in RPM 4.17+

Currently, we:

1. Load %{SOURCE102} if it exists.
   This should always be the case when actually building the RPM or SRPM package.

2. Else, load macros.python-srpm if it exists.
   This is the case when something parses the spec from dist-git without setting
   %_sourcedir to the current working directory. E.g. rpmdev-bumpspec does this.

3. Else, don't load anything, get %{__default_python3_version} from the environment.
   This is the case when something parses the spec in isolation.
   Getting the version from sources is impossible, because the sources are missing.
   So we get the installed version instead. Note that this will blow up on Fedora < 33,
   but it already did before.
2021-04-27 11:58:15 +02:00
Miro Hrončok 187e049d6c Document a TESTED_FILES value that currently works 2021-04-08 15:39:13 +02:00
Miro Hrončok 39166a7b4b Tests: Assert single-line macros are single-line 2021-04-08 15:33:22 +02:00
Karolina Surma 9d2fcef337 Use sysconfig.get_path() to define %python_sitelib and %python_sitearch
Distutils which were used to define the macros are deprecated in Python3.10:
https://www.python.org/dev/peps/pep-0632/.
Sysconfig isn't and it works across our Pythons, making it better choice for the task.
2021-04-08 14:57:19 +02:00
Miro Hrončok bd4c3de20c Make the spec file parseable when %_sourcedir is not .
This happens e.g. with:

 - rpmlint python-rpm-macros.spec
 - rpmdev-bumpspec python-rpm-macros.spec
2021-04-07 12:48:38 +02:00
Miro Hrončok dcb4422895 Escape a macro in an old %changelog entry 2021-04-07 12:48:38 +02:00
Miro Hrončok a44ae31ad0 Allow commas as argument separator for extras names in %python_extras_subpkg
This allows e.g.:

    %global extras cli,ghostwriter,pytz,dateutil,lark,numpy,pandas,pytest,redis,zoneinfo,django
    %{pyproject_extras_subpkg -n python3-hypothesis %{extras}}
    ...
    %pyproject_buildrequires -x %{extras}

(Note that %pyproject_extras_subpkg is a tiny wrapper around %python_extras_subpkg.)
2021-04-07 12:48:38 +02:00
Lumir Balhar bc016cbbc5 Make extras_subpkg description more general
Because extra subpackages actually might contain code.
See for example: https://src.fedoraproject.org/rpms/python-dns/pull-request/9
2021-03-16 13:05:31 +01:00
Kalev Lember 4805d44fa0 BRP Python Bytecompile: Avoid hardcoding /usr/bin prefix for python
Avoid using the full path and instead rely on PATH being correctly set
up to find the executable.

This fixes byte compilation for python2.7 when doing flatpak module
builds where python2.7 can be in either /usr/bin or /app/bin, depending
on how it's compiled.
2021-03-10 21:30:12 +01:00
Miro Hrončok 1edfea6956 Update comment for python_altnames() to reflect the reality 2021-02-24 13:06:35 +01:00
Miro Hrončok 8a1e9e0953 Set Version: %{__default_python3_version} to remove one place to bump
Since Fedora 33, the package version always == %{__default_python3_version}.
When we update Python to 3.X+1, we update the version and the macro.

When the macro is not updated confusing things happen.
See for example https://bugzilla.redhat.com/show_bug.cgi?id=1931421#c4

We could assert the versions match in %check instead,
but this way a temporary pull request for a new Python version, such as
https://src.fedoraproject.org/rpms/python-rpm-macros/pull-request/50
https://src.fedoraproject.org/rpms/python-rpm-macros/pull-request/84
can be kept rebased via the git forge UI.

Note that the value of %{__default_python3_version} is loaded from sources in
dist git, otherwise it would be defined by the previous build of this package.

As a result, the spec file is no longer parsable on it's own, but IMHO that's OK.
2021-02-24 13:05:56 +01:00
Miro Hrončok a6382f5b5a Fix %python_extras_subpkg with underscores in extras names
Fixes https://lists.fedoraproject.org/archives/list/packaging@lists.fedoraproject.org/thread/FI6J7JNKIOYGBYIN5UJVWYG24UIIES2U/
2021-02-20 12:52:02 +01:00
Miro Hrončok 626168789c Remove python2-rpm-macros
https://fedoraproject.org/wiki/Changes/Disable_Python_2_Dist_RPM_Generators_and_Freeze_Python_2_Macros

This is to be shipped together with an upgrade of python2.7:
The python2.7 RPM package will contain the removed macros instead.

The release is intentionally over-bumped to allow some changes of
python-rpm-macros in lower versions of Fedora without the need to bump
the version-release of python2-rpm-macros obsoleted by python2.7.
2021-02-08 12:11:39 +01:00
Miro Hrončok 230ce7f061 Tests: Amend the comment for TESTED_FILES
Arguably, this is easier.
2021-02-08 12:04:29 +01:00
Miro Hrončok c746b25f28 Automatically word-wrap the description of extras subpackages
This only works for package and extra names less than 79 characters long.
I don't expect many actual packages to exceed this limit.
78 characters should be enough for everybody.

Why 78? 79 is the line-lenght limit from rpmlint.
And we need to put punctuation in there.
2021-02-08 12:04:29 +01:00
Fedora Release Engineering 5b5cc39d89 - Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild
Signed-off-by: Fedora Release Engineering <releng@fedoraproject.org>
2021-01-27 13:13:29 +00:00
Miro Hrončok e5429a7a48 Support defining %py3_shebang_flags to %nil 2020-12-09 11:39:08 +01:00
Miro Hrončok a27bc6cc24 BRP Python Bytecompile: Also detect Python files in /app/lib/pythonX.Y
This is needed for flatpaks.

Alternatively, we could pass %{_prefix} as an argument to this script,
but that could make things a tad more complicated.

This solution is less general, but more pragmatic.

See https://lists.fedoraproject.org/archives/list/devel@lists.fedoraproject.org/thread/4FBBB3C5E63VDNGUJRLLW27LPZ74SEJH/
2020-11-29 17:44:37 +01:00
Miro Hrončok 06987f5024 Add %python3_platform_triplet and %python3_ext_suffix
Also add %python_platform_triplet and %python_ext_suffix.

The CI tests are limited to x86_64 for now.

https://fedoraproject.org/wiki/Changes/Python_Upstream_Architecture_Names
2020-09-16 17:32:39 +02:00
Miro Hrončok a712d455f8 python3-devel is required on the CI for pythontest.spec 2020-09-16 17:32:39 +02:00
Lumir Balhar 0253654076 Use versioned pytest executable in `%pytest` macro for non-main Python stack 2020-09-10 07:51:07 +02:00
Lumir Balhar 431e4380cc Add a test for %py_byte_compile macro 2020-09-07 11:20:40 +02:00
Lumir Balhar 2eb41fe707 Implement an environment variable to run tests with specific macros 2020-07-24 13:16:23 +02:00
Lumir Balhar 1979a78de9 Adapt %py(3)_shebang_fix to use versioned pathfixX.Y.py
Versioned pathfixX.Y.py is available in main as well as in
alternative Pythons so this change enables to build
an alternative Python stack without a dependency on the main
python3-devel.
2020-07-24 13:13:56 +02:00
Lumir Balhar 0eae1d90da Disable Python hash seed randomization in %py_byte_compile
This change should help with byte-compilation reproducibility: https://bugzilla.redhat.com/show_bug.cgi?id=1686078
2020-07-24 07:35:07 +02:00
Lumir Balhar 71c410dfa9 Disable Python hash seed randomization in brp-python-bytecompile
This change should help with byte-compilation reproducibility: https://bugzilla.redhat.com/show_bug.cgi?id=1686078
2020-07-23 12:37:34 +02:00
Lumir Balhar 638f809f4c Make %py3_dist respect %python3_pkgversion
By default, %{py3_dist foo} generates python3dist(foo).
This change makes it respect %python3_pkgversion so when
it is redefined as X.Y, %{py3_dist foo} generates pythonX.Y(foo).
2020-07-21 13:42:58 +02:00
Lumir Balhar b983c2118b New opt-in possibility to fix byte-compilation reproducibility
A new script brp-fix-pyc-reproducibility creates an opt-in way of how to fix
problems with the reproducibility of byte-compiled Python files. The script
uses marshalparser [0] which currently doesn't provide solutions for all issues
but can fix at least problems with reference flags. For more info see
this Bugzilla [1].

If you want to use this new feature, you need to define
`%py_reproducible_pyc_path` to specify a path you want to fix `.pyc`
files in (recursively) and build-require /usr/bin/marshalparser.

if you forget to build-require the parser. The error message is:
```
+ /usr/lib/rpm/redhat/brp-python-bytecompile '' 1 0
Bytecompiling .py files below /builddir/build/BUILDROOT/tldr-0.5-2.fc33.x86_64/usr/lib/python3.9 using /usr/bin/python3.9
+ /usr/lib/rpm/redhat/brp-fix-pyc-reproducibility /builddir/build/BUILDROOT/tldr-0.5-2.fc33.x86_64
ERROR: If %py_reproducible_pyc_path is defined, you have to also BuildRequire: /usr/bin/marshalparser !
error: Bad exit status from /var/tmp/rpm-tmp.UUJr4v (%install)
```

A build fails if the parser is not able to parse any of the `.pyc` files.

And finally, if a build is properly configured it produces fixed `.pyc` files.

Currently, `.pyc` files in the tldr package contain a lot of unused reference flags:
```
$ dnf install -y tldr
$ marshalparser --unused /usr/lib/python3.9/site-packages/__pycache__/tldr.cpython-39.pyc
… long output …
190 - Flag_ref(byte=9610, type='TYPE_SHORT_ASCII_INTERNED', content=b'init', usages=0)
191 - Flag_ref(byte=9633, type='TYPE_SHORT_ASCII_INTERNED', content=b'source', usages=0)
192 - Flag_ref(byte=9651, type='TYPE_SHORT_ASCII_INTERNED', content=b'argv', usages=0)
193 - Flag_ref(byte=9657, type='TYPE_SHORT_ASCII_INTERNED', content=b'print_help', usages=0)
194 - Flag_ref(byte=9669, type='TYPE_SHORT_ASCII_INTERNED', content=b'stderr', usages=0)
195 - Flag_ref(byte=9682, type='TYPE_SHORT_ASCII_INTERNED', content=b'parse_args', usages=0)
196 - Flag_ref(byte=9737, type='TYPE_SHORT_ASCII_INTERNED', content=b'encode', usages=0)
197 - Flag_ref(byte=9782, type='TYPE_SHORT_ASCII_INTERNED', content=b'parser', usages=0)
198 - Flag_ref(byte=9790, type='TYPE_SHORT_ASCII_INTERNED', content=b'options', usages=0)
199 - Flag_ref(byte=9799, type='TYPE_SHORT_ASCII_INTERNED', content=b'rest', usages=0)
200 - Flag_ref(byte=9821, type='TYPE_SHORT_ASCII_INTERNED', content=b'result', usages=0)
202 - Flag_ref(byte=10022, type='TYPE_SHORT_ASCII_INTERNED', content=b'__main__', usages=0)
203 - Flag_ref(byte=10102, type='TYPE_SHORT_ASCII_INTERNED', content=b'argparse', usages=0)
204 - Flag_ref(byte=10433, type='TYPE_SHORT_ASCII_INTERNED', content=b'__name__', usages=0)
205 - Flag_ref(byte=10463, type='TYPE_SHORT_ASCII_INTERNED', content=b'<module>', usages=0)
```

This new feature fixes them:

```
$ marshalparser --unused /usr/lib/python3.9/site-packages/__pycache__/tldr.cpython-39.pyc
<empty output>
```

[0] https://github.com/fedora-python/marshalparser
[1] https://bugzilla.redhat.com/show_bug.cgi?id=1686078
2020-07-21 11:42:54 +00:00
Miro Hrončok 06ee391993 Add --no-index --no-warn-script-location pip options to %pyX_install_wheel
--no-index saves a ~1 minute timeout of pip possibly checking if there is
a newer pip version (in mock with disabled network).

--no-warn-script-location removes a bogus warning that says:

    WARNING: The scripts easy_install and easy_install-3.9 are installed in
    '/builddir/build/BUILDROOT/python-setuptools-49.2.0-1.fc33.x86_64/usr/bin'
    which is not on PATH.
    Consider adding this directory to PATH or, if you prefer to suppress this warning,
    use --no-warn-script-location.
2020-07-20 18:13:49 +02:00
Miro Hrončok 0086612c98 Define %python_platform (as a Python version agnostic option to %python3_platform)
%python2_platform is also defined, for consistency.
2020-07-20 18:13:44 +02:00
Miro Hrončok 69b1b30d53 Make the unversioned %__python macro error
See https://fedoraproject.org/wiki/Changes/PythonMacroError

While doing it, make %python macros more consistent with %python3 macros,
mostly wrt whitespace but also to use python -m pip over plain pip etc.

One significant change is the removal of sleeps from python macros,
this could affect packages that use python macros to build for Python 2
while also using python3 macros to build for Python 3.
In reality, I consider that unlikely. The sleep in python2 macros stays.

The --strip-file-prefix option was already removed from %pyX_install_wheel
but we forgot to remove it from %py_install_wheel.
2020-07-16 18:45:25 +02:00
Miro Hrončok 59abe832d4 Adapt %%py_dist_name to keep square brackets
So %{py3_dist foo[bar]} works as expected.

Add tests.
2020-07-10 15:58:00 +02:00
Miro Hrončok 763d24cc5c Add %python_extras_subpkg
See https://fedoraproject.org/wiki/Changes/PythonExtras
2020-07-09 00:49:15 +02:00
Lumir Balhar 3a211cc91b Use compileall from stdlib for Python >= 3.9 2020-06-16 13:57:38 +02:00
Lumir Balhar 8e9c3d8bbe Use compileall from stdlib for Python >= 3.9
All enhancements from compileall2 are merged in Python 3.9.
2020-06-16 13:54:43 +02:00
Lumir Balhar 437166cca7 Remove trailing whitespace 2020-06-15 16:20:07 +02:00
Lumir Balhar f77cb3e9dd No more automagic Python bytecompilation (phase 3) 2020-06-15 16:20:05 +02:00
Miro Hrončok 985a80572f 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-11 20:51:34 +02:00
Miro Hrončok bae52eafbe Require the exact same version-release of other subpackages of this package
To avoid mock using a local copy of python-srpm-macros from the buildroot cache
and having Python version defined to 3.8 until the cache expires.

Also, other surprises happened in the past, so we stop playing it cool and
actually follow the guidelines here.
2020-05-31 00:28:19 +02:00
Miro Hrončok 5919708f6c https://fedoraproject.org/wiki/Changes/Python3.9
Also switch to PEP 503 based %py_dist_name (see rhbz#1791530)
2020-05-21 17:39:01 +02:00
Miro Hrončok 72371929c5 Implement %pyX_shebang_fix
See https://lists.fedoraproject.org/archives/list/python-devel@lists.fedoraproject.org/thread/UGCMDDG3S32U7JJK36OEZNHLUVQQRG3M/
2020-05-18 18:58:39 +02:00
Miro Hrončok 4569c61d8d Strip tildes from %version in %pypi_source by default, add tests 2020-05-12 11:05:30 +02:00
Miro Hrončok 0d3f1e6b74 Implement %pytest
See https://lists.fedoraproject.org/archives/list/python-devel@lists.fedoraproject.org/thread/XLPDSH362PJKMJCAYOXNJNV53Y66EF6B/
2020-05-11 19:04:28 +02:00
Miro Hrončok 5f3e4d6300 Change %__default_python3_pkgversion from 38 to 3.8
See https://lists.fedoraproject.org/archives/list/python-devel@lists.fedoraproject.org/message/VIUS7WMQMDX6H2WEIH7TVTMBB6SUHY7E/
2020-05-07 21:47:38 +02:00
Miro Hrončok 76aecd9ad7 Require recent enough SRPM macros from RPM macros, to prevent missing Lua files
%pyhon_provide in python-rpm-macros uses new Lua functions from python-srpm-macros

People have python-srpm-macros pre-installed, and it is not autoupdated,
hence they can have older versions installed and it blows up.
2020-05-06 01:31:46 +02:00
Miro Hrončok 25355e4a16 Reuse python.python_altprovides in %python_provide
This way, the generator won't re-add the same provide again,
keeping rpmlint happy even with the old macro.
2020-05-05 13:54:28 +02:00
Miro Hrončok f5bea8c7b7 Fedora CI: Add eval tests for %python_provide and %py_provides 2020-05-05 13:54:28 +02:00
Miro Hrončok 5fe974759a Make %py_provides work repeatedly 2020-05-05 13:54:28 +02:00
Miro Hrončok 8fea79b1ec Implement %py_provides 2020-05-05 13:54:26 +02:00
Miro Hrončok 125134cf84 Add functions to be used in %py_provides and the provides generator 2020-05-05 13:53:46 +02:00
Miro Hrončok a5778bf4f2 Reorganize the spec 2020-05-05 13:53:46 +02:00
Miro Hrončok b314efc5a7 Add common Lua functions, use a Lua function in %python_provide 2020-05-05 13:53:46 +02:00
Tomas Hrnciar daf7d32612 remove direct_url.json file and sed it out from RECORD
With PEP 610 there is created new file direct_url.json since is not
useful for us, it will be removed using py3_install_wheel macro.

See: https://discuss.python.org/t/pep-610-usage-guidelines-for-linux-distributions/4012
2020-05-05 09:18:16 +02:00
Miro Hrončok fed99a0478 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-27 10:40:14 +02:00
Lumir Balhar 15e32a3005 Update of bundled compileall2 module to 0.7.1 (bugfix release) 2020-03-31 14:24:09 +02:00
Miro Hrončok 1b3e731dc6 Brand as "3.8", rework %python_provide
- Hardcode the default Python 3 version in the SRPM macros
- Provide python38-foo for python3-foo and the other way around (future RHEL compatibility)

Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1812087

    $ rpm --eval '%python_provide python38-setuptools'
    Provides: python-setuptools = %{version}-%{release}
    Provides: python3-setuptools = %{version}-%{release}
    Obsoletes: python-setuptools < %{version}-%{release}

    $ rpm --eval '%python_provide python3-setuptools'
    Provides: python-setuptools = %{version}-%{release}
    Provides: python38-setuptools = %{version}-%{release}
    Obsoletes: python-setuptools < %{version}-%{release}

    $ rpm --eval '%python_provide python39-setuptools'

    $ rpm --define 'python3_pkgversion 39' --eval '%python_provide python%{python3_pkgversion}-setuptools'

To make the implementation of %python_provide easier,
any names starting with "python" or "pypy" are recognized as valid arguments.

Previously, this was an ERROR:

    $ rpm --eval '%python_provide pythonista'
    %python_provide: ERROR: pythonista not recognized.

Now it is a no-op. The behavior was never documented and the change is
backwards compatible for working spec files.
2020-03-25 17:27:06 +01:00
Tomas Orsava 229fd899ac brp-python-bytecompile: Prepare for 2 digit minor versions (e.g. 3.10) 2020-02-14 12:35:17 +00:00
Miro Hrončok 456f3ecffb 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:33 +01:00
Miro Hrončok e9f07b72aa Define %py(2|3)?_shbang_opts_nodash to be used with pathfix.py -a 2020-02-06 10:26:04 +01:00
Fedora Release Engineering 82f62228c9 - Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild
Signed-off-by: Fedora Release Engineering <releng@fedoraproject.org>
2020-01-30 15:05:59 +00:00
Lumir Balhar fa7d708e3c Use `-B` flag for Python when using compileall2 to not write pyc files
The Python compileall2 module in /usr/lib/rpm/redhat/
can be executed by various different Python interpreters.
We don't want to write several different `*.pyc` files
to this location - in most cases, that's not possible,
but somebody might run this as root.
2020-01-27 15:32:07 +00:00
Anna Khaitovich 2314fd928a Remove stray __pycache__ directory from /usr/bin when running %py_install, %py_install_wheel and %py_install_egg macros
Solves bz#1739848
2019-12-28 19:16:16 +01:00
Miro Hrončok 6c63a5b7f4 Add the %pycached macro
Usage:

  %files
  ...
  %pycached %{python3_sitelib}/foo.py

This will list:

  /usr/lib/python3.8/site-packages/foo.py
  /usr/lib/python3.8/site-packages/__pycache__/foo.cpython-38{,.opt-?}.pyc

Assuming the Python 3 version is 3.8.
The bytecode files are globbed, their presence is not checked.

This will fail:

  %pycached %{python3_sitelib}/foo

error: %pycached can only be used with paths explicitly ending with .py

And so will any of this:

  %pycached %{python3_sitelib}/*
  %pycached %{python3_sitelib}/foo.*
  %pycached %{python3_sitelib}/foo.p?
  %pycached %{python3_sitelib}/foo.?y
  %pycached %{python3_sitelib}/foo.??

But this will work:

  %pycached %{python3_sitelib}/foo*.py

And it will generate the following globs:

  /usr/lib/python3.8/site-packages/foo*.py
  /usr/lib/python3.8/site-packages/__pycache__/foo*.cpython-38{,.opt-?}.pyc

When used with paths that include Python 3 version, it globs with the version:

  %pycached /opt/python3.10/foo.py

Generates:

  /opt/python3.10/foo.py
  /opt/python3.10/__pycache__/foo.cpython-310{,.opt-?}.pyc

While paths without version have less strict globs:

  %pycached /custom/foo.py
  /custom/foo.py
  /custom/__pycache__/foo.cpython-3*{,.opt-?}.pyc

This will generate a warning in RPM build:

warning: File listed twice: /custom/__pycache__/foo.cpython-38.opt-1.pyc

However it ensures the optimized bytecode is there.
2019-12-28 19:12:37 +01:00
Miro Hrončok f0be0a2983 Define %python, but make it work only if %__python is redefined 2019-12-28 19:10:09 +01:00
Lumir Balhar 8f6bc2fd6c Fix brp-python-bytecompile with the new features from compileall2
Resolves: rhbz#1595265

The problem this change is intended to solve is with how `real_libdir`
is calculated. Let's assume we want to recursively byte-compile all
`*.py` files in
`/builddir/build/BUILDROOT/python-scales-1.0.9-250.fc32.x86_64/usr/lib/python3.8`.
Then, `real_libdir` is this path without `$RPM_BUILD_ROOT` with
the filename at the end which displays in the error message like this:

```
Bytecompiling .py files below /builddir/build/BUILDROOT/python-scales-1.0.9-250.fc32.x86_64/usr/lib/python3.8 using /usr/bin/python3.8
*** Error compiling '/builddir/build/BUILDROOT/python-scales-1.0.9-250.fc32.x86_64/usr/lib/python3.8/site-packages/greplin/bar.py'...
  File "/usr/lib/python3.8/bar.py", line 1
    import sin from math
               ^
SyntaxError: invalid syntax
```

`/usr/lib/python3.8/bar.py` is obviously wrong.

One of the new features of the `compileall2` module (which will
be available in stdlib in Python 3.9) is that the path byte-compiled to
`*.pyc` files is calculated for each file. This means that by using
`-s` and `-p` we can strip `$RPM_BUILD_ROOT` and prepend `/` for each
file individually which will fix the problem.

```
Bytecompiling .py files below /builddir/build/BUILDROOT/python-scales-1.0.9-250.fc32.x86_64/usr/lib/python3.8 using /usr/bin/python3.8
*** Error compiling '/builddir/build/BUILDROOT/python-scales-1.0.9-250.fc32.x86_64/usr/lib/python3.8/site-packages/greplin/bar.py'...
  File "/usr/lib/python3.8/site-packages/greplin/bar.py", line 1
    import sin from math
               ^
SyntaxError: invalid syntax
```

This change has an effect only for Python >= 3.4.
2019-12-02 16:39:32 +01:00
Lumir Balhar bebf85d28b Bundled compileall2 module update to 0.6.0 2019-11-26 14:22:54 +01:00
Miro Hrončok af35bb0ac9 Define %python2 and %python3
See https://pagure.io/packaging-committee/issue/907

Redefine %__pythonX to change the behavior of %pythonX, %pythonX_version, etc.
Use %pythonX in spec.
2019-09-27 10:02:49 +02:00
Miro Hrončok f09ccd21f5 Drop --strip-file-prefix option from %pyX_install_wheel macros, it is not needed
A custom pip patch was needed for this option, but the RECORD files are
relative, so no stripping is needed. We will eventually drop the patch.
2019-08-26 15:33:41 +00:00
Miro Hrončok 84ed1ab69d Fix %python3_version macros for Python 3.10
No need to bump the release, 3.10 is far from now.

Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1745601
2019-08-26 15:24:23 +02:00
Fedora Release Engineering 2cbca3f95e - Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild
Signed-off-by: Fedora Release Engineering <releng@fedoraproject.org>
2019-07-26 16:06:16 +00:00
Lumir Balhar e64ffd7f26 Use compileall2 Python module for byte-compilation in brp-python-bytecompile 2019-07-20 08:08:26 +02:00
Miro Hrončok e1bd214d25 Move brp-python-bytecompile from rpm, so we can easily adapt it 2019-07-20 08:07:57 +02:00
Lumir Balhar eb3274394c Do not allow passing arguments to Python during byte-compilation 2019-07-15 14:26:49 +02:00
Lumir Balhar 4493789fb8 Use `-s` to not add user site directory to sys.path for byte-compilation 2019-07-15 14:26:49 +02:00
Lumir Balhar 76681ad58e Use a new module compileall2 for Python byte-compilation 2019-07-15 14:26:49 +02:00
Miro Hrončok 64119cef2c Switch %python_provide behavior between Python 2 <--> 3
https://fedoraproject.org/wiki/Changes/Python_means_Python3

Welcome to the future.
2019-07-15 13:22:03 +02:00
Miro Hrončok b67b47d5b0 %python_provide: Don't try to obsolete %_isa provides
Based on recent changes in 04769fa014,
packagers might want to use:

    %{?python_provide:%python_provide python2-foo%{?_isa}}

...for backwards compatibility. However the macro adds obsoletes and
since RPM 4.15, obsoletes with %{?_isa} are not possible:

    Only package names are allowed in Obsoletes: Obsoletes: python-foo(x86-64) < ...

To allow such usage, %python_provide now only obsoletes if the argument
does not end with ")".
2019-07-09 13:45:18 +02:00
Miro Hrončok cf8051e7f5 Make %__python /usr/bin/python once again until we are ready
See https://src.fedoraproject.org/rpms/python-rpm-macros/pull-request/22#comment-26552
and further.
2019-06-17 16:29:44 +02:00
Miro Hrončok 04769fa014 Remove the arched provide from %python_provide macro
The way it fetched the information abut archfulness was not reliable
and will stop working entirely.

See https://bugzilla.redhat.com/show_bug.cgi?id=1705656
2019-06-17 09:34:06 +02:00
Miro Hrončok 536b2efe4e Move %__python definition to the srpm macros, so it is always present 2019-06-12 11:34:53 +02:00
Miro Hrončok d38048d54d Define %python_sitelib, %python_sitearch, %python_version, %python_version_nodots
In rpm 4.15 those are no longer defined.

The meaning of "python" is derived from %__python - and that errors by default
unless user defined.

Example usage:

    %global __python /usr/bin/pypy3

    ...

    %files
    %{python_sitelib}/foo/
2019-06-10 15:40:53 +02:00
Fedora Release Engineering 813a86fcc6 - Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild
Signed-off-by: Fedora Release Engineering <releng@fedoraproject.org>
2019-02-02 09:09:45 +00:00
Igor Gnatenko 9b8fac037d
Add %python_disable_dependency_generator
Signed-off-by: Igor Gnatenko <ignatenkobrain@fedoraproject.org>
2018-12-20 14:35:02 +01:00
Miro Hrončok 4b3c23b233 Workaround leaking buildroot PATH in %py_byte_compile (#1647212) 2018-12-14 10:20:40 +01:00
Petr Viktorin c8932dcbef Move "sleep 1" workaround from py3_build to py2_build
https://bugzilla.redhat.com/show_bug.cgi?id=1644923
2018-11-01 11:06:22 +01:00
Tomas Orsava beaa2eec4f Move the __python2/3 macros to the python-srpm-macros subpackage
This facilitates using the %%{__python2/3} in Build/Requires
2018-09-20 13:36:47 +02:00
Miro Hrončok 8f067ff385 Make %py_byte_compile terminate build on SyntaxErrors (#1616219)
Also, make it simpler again
2018-08-22 11:48:27 +02:00
Miro Hrončok 25b297c006 Bump for 90b0bf7 2018-08-15 11:56:37 +02:00
Pavel Raiskup 90b0bf7480 macros.python: drop last %__python2 occurrence
This is needed so people can do things like:

    %if %{with python3}
    %global __python %__python3
    %else
    %global __python %__python2
    %else

And than just use %__python, %py_build, %py_install,
%python_sitelib, ...
2018-08-03 17:08:55 +02:00
Igor Gnatenko d3d040818d
Change way how enabling-depgen works internally
Signed-off-by: Igor Gnatenko <ignatenkobrain@fedoraproject.org>
2018-07-28 14:26:10 +02:00
Tomas Orsava 05333eb586 macros.pybytecompile: Detect Python version through sys.version_info
...instead of guessing from the executable name.

This should make it work on EPEL7 as well where we ship 3.4 and 3.6

https://bugzilla.redhat.com/show_bug.cgi?id=1599809
2018-07-19 11:52:36 +02:00
Fedora Release Engineering 967bb3c12e - Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild
Signed-off-by: Fedora Release Engineering <releng@fedoraproject.org>
2018-07-14 01:58:10 +00:00
Tomas Orsava f961fbbc29 Fix %%py_byte_compile macro
When invoked with a Python 2 binary it also mistakenly ran py3_byte_compile.
2018-07-10 15:14:23 +02:00
Miro Hrončok 1ea9947b6f Add %python3_platform useful for PYTHONPATH on arched builds
This is mostly used when building Sphinx docs on arched build.

Previously:

  PLATFORM=$(python3 -c "import sysconfig; print(sysconfig.get_platform())")
  export PYTHONPATH=../build/lib.${PLATFORM}-%{python3_version}
  make man

Or:

  PYTHONPATH=`realpath ../build/lib.linux*` make

Or:

  PYTHONPATH=$(echo $PWD/build/lib.linux-*) make html

Now:

  PYTHONPATH=../build/lib.%{python3_platform}-%{python3_version}
2018-07-03 14:21:49 +02:00
Jason Tibbitts 37a004eed8 Add %pypi_source macro.
Adds the %pypi_source macro, as well as %__pypi_url and
%__pypi_default_extension.  This should make references to sources in
PyPI much simpler for nearly all Python packages.
2018-06-18 12:54:12 -05:00
Miro Hrončok 3c79d6a899 Move macros.pybytecompile from python3-devel
- https://fedoraproject.org/wiki/Changes/No_more_automagic_Python_bytecompilation
 - No conflicts need to be added the file is renamed
 - If both files are present, there is no harm, so we'll just remove it from 3.7
2018-04-18 18:36:19 +02:00
Miro Hrončok 21ac2a9653 Make the pybytecompile version agnostic again
It is no longer bound to a specific pythonX package
2018-04-10 11:58:45 +02:00
Miro Hrončok 438faf7c86 Fix the py_byte_compile macro to work on Python 2
See https://bugzilla.redhat.com/show_bug.cgi?id=1484993

Inspired by Terje Røsten's workaround from that bugzilla
2018-04-10 11:58:45 +02:00
Tomas Orsava 2e3d372310 Fix syntax error in %py_byte_compile macro (rhbz#1433569) 2018-04-10 11:58:45 +02:00
Charalampos Stratakis 5208f057c1 Update macros.pybytecompile to 3.6
Rename the macros.pybytecompile3.5 file to macros.pybytecompile3.6
2018-04-10 11:58:45 +02:00
Charalampos Stratakis 3af7df6454 Update %py_byte_compile macro 2018-04-10 11:58:45 +02:00
Robert Kuska 99ca83f054 Rename macros to 3.5 2018-04-10 11:58:45 +02:00
Matej Stuchlik bcfeb6a0b1 Update macros.pybytecompile to 3.4 2018-04-10 11:58:45 +02:00
dmalcolm 739136717f Introduce macros.pybytecompile 2018-04-10 11:58:45 +02:00
18 changed files with 3211 additions and 130 deletions

View File

@ -0,0 +1,20 @@
#!/bin/bash -e
# If using normal root, avoid changing anything.
if [ -z "$RPM_BUILD_ROOT" -o "$RPM_BUILD_ROOT" = "/" ]; then
exit 0
fi
# Defined as %py_reproducible_pyc_path macro and passed here as
# the first command-line argument
path_to_fix=$1
# First, check that the parser is available:
if [ ! -x /usr/bin/marshalparser ]; then
echo "ERROR: If %py_reproducible_pyc_path is defined, you have to also BuildRequire: /usr/bin/marshalparser !"
exit 1
fi
# Set pipefail so if $path_to_fix does not exist, the build fails
set -o pipefail
find "$path_to_fix" -type f -name "*.pyc" | xargs /usr/bin/marshalparser --fix --overwrite

130
brp-python-bytecompile Normal file
View File

@ -0,0 +1,130 @@
#!/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
# If using normal root, avoid changing anything.
if [ -z "$RPM_BUILD_ROOT" -o "$RPM_BUILD_ROOT" = "/" ]; then
exit 0
fi
# This function now implements Python byte-compilation in three different ways:
# Python >= 3.4 and < 3.9 uses a new module compileall2 - https://github.com/fedora-python/compileall2
# In Python >= 3.9, compileall2 was merged back to standard library (compileall) so we can use it directly again.
# Python < 3.4 (inc. Python 2) uses compileall module from stdlib with some hacks
function python_bytecompile()
{
local options=$1
local python_binary=$2
local exclude=$3
local python_libdir="$4"
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
[ ! -z $exclude ] && exclude="-x '$exclude'"
# PYTHONPATH is needed for compileall2, but doesn't hurt for the stdlib
# -o 0 -o 1 are the optimization levels
# -q disables verbose output
# -f forces the process to overwrite existing compiled files
# -x excludes paths defined by regex
# -e excludes symbolic links pointing outside the build root
# -x and -e together implements the same functionality as the Filter class below
# -s strips $RPM_BUILD_ROOT from the path
# -p prepends the leading slash to the path to make it absolute
PYTHONPATH=/usr/lib/rpm/redhat/ $python_binary -B -m $compileall_module -o 0 -o 1 -q -f $exclude -s "$RPM_BUILD_ROOT" -p / --hardlink-dupes -e "$RPM_BUILD_ROOT" "$python_libdir"
else
#
# Python 3.3 and lower (incl. Python 2)
#
local real_libdir=${python_libdir/$RPM_BUILD_ROOT/}
cat << EOF | $python_binary $options
import compileall, sys, os, re
python_libdir = "$python_libdir"
depth = sys.getrecursionlimit()
real_libdir = "$real_libdir"
build_root = "$RPM_BUILD_ROOT"
exclude = r"$exclude"
class Filter:
def search(self, path):
ret = not os.path.realpath(path).startswith(build_root)
if exclude:
ret = ret or re.search(exclude, path)
return ret
sys.exit(not compileall.compile_dir(python_libdir, depth, real_libdir, force=1, rx=Filter(), quiet=1))
EOF
fi
}
# .pyc/.pyo files embed a "magic" value, identifying the ABI version of Python
# bytecode that they are for.
#
# The files below RPM_BUILD_ROOT could be targeting multiple versions of
# python (e.g. a single build that emits several subpackages e.g. a
# python26-foo subpackage, a python31-foo subpackage etc)
#
# Support this by assuming that below each /usr/lib/python$VERSION/, all
# .pyc/.pyo files are to be compiled for /usr/bin/python$VERSION.
#
# For example, below /usr/lib/python2.6/, we're targeting /usr/bin/python2.6
# and below /usr/lib/python3.1/, we're targeting /usr/bin/python3.1
# Disable Python hash seed randomization
# This should help with byte-compilation reproducibility: https://bugzilla.redhat.com/show_bug.cgi?id=1686078
# Python 3.11+ no longer needs this: https://github.com/python/cpython/pull/27926 (but we support older Pythons as well)
export PYTHONHASHSEED=0
shopt -s nullglob
find "$RPM_BUILD_ROOT" -type d -print0|grep -z -E "/(usr|app)/lib(64)?/python[0-9]\.[0-9]+$" | while read -d "" python_libdir;
do
python_binary=$(basename "$python_libdir")
echo "Bytecompiling .py files below $python_libdir using $python_binary"
# Generate normal (.pyc) byte-compiled files.
python_bytecompile "" "$python_binary" "" "$python_libdir"
if [ $? -ne 0 -a 0$errors_terminate -ne 0 ]; then
# One or more of the files had a syntax error
exit 1
fi
# Generate optimized (.pyo) byte-compiled files.
# N.B. For Python 3.4+, this call does nothing
python_bytecompile "-O" "$python_binary" "" "$python_libdir"
if [ $? -ne 0 -a 0$errors_terminate -ne 0 ]; then
# One or more of the files had a syntax error
exit 1
fi
done

25
brp-python-hardlink Normal file
View File

@ -0,0 +1,25 @@
#!/bin/sh
# If using normal root, avoid changing anything.
if [ -z "$RPM_BUILD_ROOT" ] || [ "$RPM_BUILD_ROOT" = "/" ]; then
exit 0
fi
hardlink_if_same() {
if cmp -s "$1" "$2" ; then
ln -f "$1" "$2"
return 0
fi
return 1
}
# Hardlink identical *.pyc, *.pyo, and *.opt-[12].pyc.
# Originally from PLD's rpm-build-macros
find "$RPM_BUILD_ROOT" -type f -name "*.pyc" -not -name "*.opt-[12].pyc" | while read pyc ; do
hardlink_if_same "$pyc" "${pyc%c}o"
o1pyc="${pyc%pyc}opt-1.pyc"
hardlink_if_same "$pyc" "$o1pyc"
o2pyc="${pyc%pyc}opt-2.pyc"
hardlink_if_same "$pyc" "$o2pyc" || hardlink_if_same "$o1pyc" "$o2pyc"
done
exit 0

515
compileall2.py Normal file
View File

@ -0,0 +1,515 @@
"""Module/script to byte-compile all .py files to .pyc files.
When called as a script with arguments, this compiles the directories
given as arguments recursively; the -l option prevents it from
recursing into directories.
Without arguments, if compiles all modules on sys.path, without
recursing into subdirectories. (Even though it should do so for
packages -- for now, you'll have to deal with packages separately.)
See module py_compile for details of the actual byte-compilation.
License:
Compileall2 is an enhanced copy of Python's compileall module
and it follows Python licensing. For more info see: https://www.python.org/psf/license/
"""
import os
import sys
import importlib.util
import py_compile
import struct
import filecmp
from functools import partial
from pathlib import Path
# Python 3.7 and higher
PY37 = sys.version_info[0:2] >= (3, 7)
# Python 3.6 and higher
PY36 = sys.version_info[0:2] >= (3, 6)
# Python 3.5 and higher
PY35 = sys.version_info[0:2] >= (3, 5)
# Python 3.7 and above has a different structure and length
# of pyc files header. Also, multiple ways how to invalidate pyc file was
# introduced in Python 3.7. These cases are covered by variables here or by PY37
# variable itself.
if PY37:
pyc_struct_format = '<4sll'
pyc_header_lenght = 12
pyc_header_format = (pyc_struct_format, importlib.util.MAGIC_NUMBER, 0)
else:
pyc_struct_format = '<4sl'
pyc_header_lenght = 8
pyc_header_format = (pyc_struct_format, importlib.util.MAGIC_NUMBER)
__all__ = ["compile_dir","compile_file","compile_path"]
def optimization_kwarg(opt):
"""Returns opt as a dictionary {optimization: opt} for use as **kwarg
for Python >= 3.5 and empty dictionary for Python 3.4"""
if PY35:
return dict(optimization=opt)
else:
# `debug_override` is a way how to enable optimized byte-compiled files
# (.pyo) in Python <= 3.4
if opt:
return dict(debug_override=False)
else:
return dict()
def _walk_dir(dir, maxlevels, quiet=0):
if PY36 and quiet < 2 and isinstance(dir, os.PathLike):
dir = os.fspath(dir)
else:
dir = str(dir)
if not quiet:
print('Listing {!r}...'.format(dir))
try:
names = os.listdir(dir)
except OSError:
if quiet < 2:
print("Can't list {!r}".format(dir))
names = []
names.sort()
for name in names:
if name == '__pycache__':
continue
fullname = os.path.join(dir, name)
if not os.path.isdir(fullname):
yield fullname
elif (maxlevels > 0 and name != os.curdir and name != os.pardir and
os.path.isdir(fullname) and not os.path.islink(fullname)):
yield from _walk_dir(fullname, maxlevels=maxlevels - 1,
quiet=quiet)
def compile_dir(dir, maxlevels=None, ddir=None, force=False,
rx=None, quiet=0, legacy=False, optimize=-1, workers=1,
invalidation_mode=None, stripdir=None,
prependdir=None, limit_sl_dest=None, hardlink_dupes=False):
"""Byte-compile all modules in the given directory tree.
Arguments (only dir is required):
dir: the directory to byte-compile
maxlevels: maximum recursion level (default `sys.getrecursionlimit()`)
ddir: the directory that will be prepended to the path to the
file as it is compiled into each byte-code file.
force: if True, force compilation, even if timestamps are up-to-date
quiet: full output with False or 0, errors only with 1,
no output with 2
legacy: if True, produce legacy pyc paths instead of PEP 3147 paths
optimize: int or list of optimization levels or -1 for level of
the interpreter. Multiple levels leads to multiple compiled
files each with one optimization level.
workers: maximum number of parallel workers
invalidation_mode: how the up-to-dateness of the pyc will be checked
stripdir: part of path to left-strip from source file path
prependdir: path to prepend to beggining of original file path, applied
after stripdir
limit_sl_dest: ignore symlinks if they are pointing outside of
the defined path
hardlink_dupes: hardlink duplicated pyc files
"""
ProcessPoolExecutor = None
if ddir is not None and (stripdir is not None or prependdir is not None):
raise ValueError(("Destination dir (ddir) cannot be used "
"in combination with stripdir or prependdir"))
if ddir is not None:
stripdir = dir
prependdir = ddir
ddir = None
if workers is not None:
if workers < 0:
raise ValueError('workers must be greater or equal to 0')
elif workers != 1:
try:
# Only import when needed, as low resource platforms may
# fail to import it
from concurrent.futures import ProcessPoolExecutor
except ImportError:
workers = 1
if maxlevels is None:
maxlevels = sys.getrecursionlimit()
files = _walk_dir(dir, quiet=quiet, maxlevels=maxlevels)
success = True
if workers is not None and workers != 1 and ProcessPoolExecutor is not None:
workers = workers or None
with ProcessPoolExecutor(max_workers=workers) as executor:
results = executor.map(partial(compile_file,
ddir=ddir, force=force,
rx=rx, quiet=quiet,
legacy=legacy,
optimize=optimize,
invalidation_mode=invalidation_mode,
stripdir=stripdir,
prependdir=prependdir,
limit_sl_dest=limit_sl_dest),
files)
success = min(results, default=True)
else:
for file in files:
if not compile_file(file, ddir, force, rx, quiet,
legacy, optimize, invalidation_mode,
stripdir=stripdir, prependdir=prependdir,
limit_sl_dest=limit_sl_dest,
hardlink_dupes=hardlink_dupes):
success = False
return success
def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0,
legacy=False, optimize=-1,
invalidation_mode=None, stripdir=None, prependdir=None,
limit_sl_dest=None, hardlink_dupes=False):
"""Byte-compile one file.
Arguments (only fullname is required):
fullname: the file to byte-compile
ddir: if given, the directory name compiled in to the
byte-code file.
force: if True, force compilation, even if timestamps are up-to-date
quiet: full output with False or 0, errors only with 1,
no output with 2
legacy: if True, produce legacy pyc paths instead of PEP 3147 paths
optimize: int or list of optimization levels or -1 for level of
the interpreter. Multiple levels leads to multiple compiled
files each with one optimization level.
invalidation_mode: how the up-to-dateness of the pyc will be checked
stripdir: part of path to left-strip from source file path
prependdir: path to prepend to beggining of original file path, applied
after stripdir
limit_sl_dest: ignore symlinks if they are pointing outside of
the defined path.
hardlink_dupes: hardlink duplicated pyc files
"""
if ddir is not None and (stripdir is not None or prependdir is not None):
raise ValueError(("Destination dir (ddir) cannot be used "
"in combination with stripdir or prependdir"))
success = True
if PY36 and quiet < 2 and isinstance(fullname, os.PathLike):
fullname = os.fspath(fullname)
else:
fullname = str(fullname)
name = os.path.basename(fullname)
dfile = None
if ddir is not None:
if not PY36:
ddir = str(ddir)
dfile = os.path.join(ddir, name)
if stripdir is not None:
fullname_parts = fullname.split(os.path.sep)
stripdir_parts = stripdir.split(os.path.sep)
ddir_parts = list(fullname_parts)
for spart, opart in zip(stripdir_parts, fullname_parts):
if spart == opart:
ddir_parts.remove(spart)
dfile = os.path.join(*ddir_parts)
if prependdir is not None:
if dfile is None:
dfile = os.path.join(prependdir, fullname)
else:
dfile = os.path.join(prependdir, dfile)
if isinstance(optimize, int):
optimize = [optimize]
if hardlink_dupes:
raise ValueError(("Hardlinking of duplicated bytecode makes sense "
"only for more than one optimization level."))
if rx is not None:
mo = rx.search(fullname)
if mo:
return success
if limit_sl_dest is not None and os.path.islink(fullname):
if Path(limit_sl_dest).resolve() not in Path(fullname).resolve().parents:
return success
opt_cfiles = {}
if os.path.isfile(fullname):
for opt_level in optimize:
if legacy:
opt_cfiles[opt_level] = fullname + 'c'
else:
if opt_level >= 0:
opt = opt_level if opt_level >= 1 else ''
opt_kwarg = optimization_kwarg(opt)
cfile = (importlib.util.cache_from_source(
fullname, **opt_kwarg))
opt_cfiles[opt_level] = cfile
else:
cfile = importlib.util.cache_from_source(fullname)
opt_cfiles[opt_level] = cfile
head, tail = name[:-3], name[-3:]
if tail == '.py':
if not force:
try:
mtime = int(os.stat(fullname).st_mtime)
expect = struct.pack(*(pyc_header_format + (mtime,)))
for cfile in opt_cfiles.values():
with open(cfile, 'rb') as chandle:
actual = chandle.read(pyc_header_lenght)
if expect != actual:
break
else:
return success
except OSError:
pass
if not quiet:
print('Compiling {!r}...'.format(fullname))
try:
for index, opt_level in enumerate(sorted(optimize)):
cfile = opt_cfiles[opt_level]
if PY37:
ok = py_compile.compile(fullname, cfile, dfile, True,
optimize=opt_level,
invalidation_mode=invalidation_mode)
else:
ok = py_compile.compile(fullname, cfile, dfile, True,
optimize=opt_level)
if index > 0 and hardlink_dupes:
previous_cfile = opt_cfiles[optimize[index - 1]]
if previous_cfile == cfile and optimize[0] not in (1, 2):
# Python 3.4 has only one .pyo file for -O and -OO so
# we hardlink it only if there is a .pyc file
# with the same content
previous_cfile = opt_cfiles[optimize[0]]
if previous_cfile != cfile and filecmp.cmp(cfile, previous_cfile, shallow=False):
os.unlink(cfile)
os.link(previous_cfile, cfile)
except py_compile.PyCompileError as err:
success = False
if quiet >= 2:
return success
elif quiet:
print('*** Error compiling {!r}...'.format(fullname))
else:
print('*** ', end='')
# escape non-printable characters in msg
msg = err.msg.encode(sys.stdout.encoding,
errors='backslashreplace')
msg = msg.decode(sys.stdout.encoding)
print(msg)
except (SyntaxError, UnicodeError, OSError) as e:
success = False
if quiet >= 2:
return success
elif quiet:
print('*** Error compiling {!r}...'.format(fullname))
else:
print('*** ', end='')
print(e.__class__.__name__ + ':', e)
else:
if ok == 0:
success = False
return success
def compile_path(skip_curdir=1, maxlevels=0, force=False, quiet=0,
legacy=False, optimize=-1,
invalidation_mode=None):
"""Byte-compile all module on sys.path.
Arguments (all optional):
skip_curdir: if true, skip current directory (default True)
maxlevels: max recursion level (default 0)
force: as for compile_dir() (default False)
quiet: as for compile_dir() (default 0)
legacy: as for compile_dir() (default False)
optimize: as for compile_dir() (default -1)
invalidation_mode: as for compiler_dir()
"""
success = True
for dir in sys.path:
if (not dir or dir == os.curdir) and skip_curdir:
if quiet < 2:
print('Skipping current directory')
else:
success = success and compile_dir(
dir,
maxlevels,
None,
force,
quiet=quiet,
legacy=legacy,
optimize=optimize,
invalidation_mode=invalidation_mode,
)
return success
def main():
"""Script main program."""
import argparse
parser = argparse.ArgumentParser(
description='Utilities to support installing Python libraries.')
parser.add_argument('-l', action='store_const', const=0,
default=None, dest='maxlevels',
help="don't recurse into subdirectories")
parser.add_argument('-r', type=int, dest='recursion',
help=('control the maximum recursion level. '
'if `-l` and `-r` options are specified, '
'then `-r` takes precedence.'))
parser.add_argument('-f', action='store_true', dest='force',
help='force rebuild even if timestamps are up to date')
parser.add_argument('-q', action='count', dest='quiet', default=0,
help='output only error messages; -qq will suppress '
'the error messages as well.')
parser.add_argument('-b', action='store_true', dest='legacy',
help='use legacy (pre-PEP3147) compiled file locations')
parser.add_argument('-d', metavar='DESTDIR', dest='ddir', default=None,
help=('directory to prepend to file paths for use in '
'compile-time tracebacks and in runtime '
'tracebacks in cases where the source file is '
'unavailable'))
parser.add_argument('-s', metavar='STRIPDIR', dest='stripdir',
default=None,
help=('part of path to left-strip from path '
'to source file - for example buildroot. '
'`-d` and `-s` options cannot be '
'specified together.'))
parser.add_argument('-p', metavar='PREPENDDIR', dest='prependdir',
default=None,
help=('path to add as prefix to path '
'to source file - for example / to make '
'it absolute when some part is removed '
'by `-s` option. '
'`-d` and `-p` options cannot be '
'specified together.'))
parser.add_argument('-x', metavar='REGEXP', dest='rx', default=None,
help=('skip files matching the regular expression; '
'the regexp is searched for in the full path '
'of each file considered for compilation'))
parser.add_argument('-i', metavar='FILE', dest='flist',
help=('add all the files and directories listed in '
'FILE to the list considered for compilation; '
'if "-", names are read from stdin'))
parser.add_argument('compile_dest', metavar='FILE|DIR', nargs='*',
help=('zero or more file and directory names '
'to compile; if no arguments given, defaults '
'to the equivalent of -l sys.path'))
parser.add_argument('-j', '--workers', default=1,
type=int, help='Run compileall concurrently')
parser.add_argument('-o', action='append', type=int, dest='opt_levels',
help=('Optimization levels to run compilation with. '
'Default is -1 which uses optimization level of '
'Python interpreter itself (specified by -O).'))
parser.add_argument('-e', metavar='DIR', dest='limit_sl_dest',
help='Ignore symlinks pointing outsite of the DIR')
parser.add_argument('--hardlink-dupes', action='store_true',
dest='hardlink_dupes',
help='Hardlink duplicated pyc files')
if PY37:
invalidation_modes = [mode.name.lower().replace('_', '-')
for mode in py_compile.PycInvalidationMode]
parser.add_argument('--invalidation-mode',
choices=sorted(invalidation_modes),
help=('set .pyc invalidation mode; defaults to '
'"checked-hash" if the SOURCE_DATE_EPOCH '
'environment variable is set, and '
'"timestamp" otherwise.'))
args = parser.parse_args()
compile_dests = args.compile_dest
if args.rx:
import re
args.rx = re.compile(args.rx)
if args.limit_sl_dest == "":
args.limit_sl_dest = None
if args.recursion is not None:
maxlevels = args.recursion
else:
maxlevels = args.maxlevels
if args.opt_levels is None:
args.opt_levels = [-1]
if len(args.opt_levels) == 1 and args.hardlink_dupes:
parser.error(("Hardlinking of duplicated bytecode makes sense "
"only for more than one optimization level."))
if args.ddir is not None and (
args.stripdir is not None or args.prependdir is not None
):
parser.error("-d cannot be used in combination with -s or -p")
# if flist is provided then load it
if args.flist:
try:
with (sys.stdin if args.flist=='-' else open(args.flist)) as f:
for line in f:
compile_dests.append(line.strip())
except OSError:
if args.quiet < 2:
print("Error reading file list {}".format(args.flist))
return False
if args.workers is not None:
args.workers = args.workers or None
if PY37 and args.invalidation_mode:
ivl_mode = args.invalidation_mode.replace('-', '_').upper()
invalidation_mode = py_compile.PycInvalidationMode[ivl_mode]
else:
invalidation_mode = None
success = True
try:
if compile_dests:
for dest in compile_dests:
if os.path.isfile(dest):
if not compile_file(dest, args.ddir, args.force, args.rx,
args.quiet, args.legacy,
invalidation_mode=invalidation_mode,
stripdir=args.stripdir,
prependdir=args.prependdir,
optimize=args.opt_levels,
limit_sl_dest=args.limit_sl_dest,
hardlink_dupes=args.hardlink_dupes):
success = False
else:
if not compile_dir(dest, maxlevels, args.ddir,
args.force, args.rx, args.quiet,
args.legacy, workers=args.workers,
invalidation_mode=invalidation_mode,
stripdir=args.stripdir,
prependdir=args.prependdir,
optimize=args.opt_levels,
limit_sl_dest=args.limit_sl_dest,
hardlink_dupes=args.hardlink_dupes):
success = False
return success
else:
return compile_path(legacy=args.legacy, force=args.force,
quiet=args.quiet,
invalidation_mode=invalidation_mode)
except KeyboardInterrupt:
if args.quiet < 2:
print("\n[interrupted]")
return False
return True
if __name__ == '__main__':
exit_status = int(not main())
sys.exit(exit_status)

171
import_all_modules.py Normal file
View File

@ -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()

53
macros.pybytecompile Normal file
View File

@ -0,0 +1,53 @@
# Note that the path could itself be a python file, or a directory
# Note that the py_byte_compile macro should work for all Python versions
# Which unfortunately makes the definition more complicated than it should be
# Usage:
# %%py_byte_compile <interpereter> <path>
# Example:
# %%py_byte_compile %%{__python3} %%{buildroot}%%{_datadir}/spam/plugins/
# This will terminate build on SyntaxErrors, if you want to avoid that,
# use it in a subshell like this:
# (%%{py_byte_compile <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()\
py2_byte_compile () {\
python_binary="env PYTHONHASHSEED=0 %1"\
bytecode_compilation_path="%2"\
failure=0\
find $bytecode_compilation_path -type f -a -name "*.py" -print0 | xargs -0 $python_binary -s -c 'import py_compile, sys; [py_compile.compile(f, dfile=f.partition("'"$RPM_BUILD_ROOT"'")[2], doraise=True) for f in sys.argv[1:]]' || failure=1\
find $bytecode_compilation_path -type f -a -name "*.py" -print0 | xargs -0 $python_binary -s -O -c 'import py_compile, sys; [py_compile.compile(f, dfile=f.partition("'"$RPM_BUILD_ROOT"'")[2], doraise=True) for f in sys.argv[1:]]' || failure=1\
test $failure -eq 0\
}\
\
py3_byte_compile () {\
python_binary="env PYTHONHASHSEED=0 %1"\
bytecode_compilation_path="%2"\
PYTHONPATH="%{_rpmconfigdir}/redhat" $python_binary -s -B -m compileall2 -o 0 -o 1 -s $RPM_BUILD_ROOT -p / --hardlink-dupes $bytecode_compilation_path \
}\
\
py39_byte_compile () {\
python_binary="env PYTHONHASHSEED=0 %1"\
bytecode_compilation_path="%2"\
$python_binary -s -B -m compileall -o 0 -o 1 -s $RPM_BUILD_ROOT -p / --hardlink-dupes $bytecode_compilation_path \
}\
\
# Path to intepreter should not contain any arguments \
[[ "%1" =~ " -" ]] && echo "ERROR py_byte_compile: Path to interpreter should not contain any arguments" >&2 && exit 1 \
# Get version without a dot (36 instead of 3.6), bash doesn't compare floats well \
python_version=$(%1 -c "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))") \
# compileall2 is an enhanced fork of stdlib compileall module for Python >= 3.4 \
# and it was merged back to stdlib in Python >= 3.9 \
if [ "$python_version" -ge 39 ]; then \
py39_byte_compile "%1" "%2"; \
elif [ "$python_version" -ge 34 ]; then \
py3_byte_compile "%1" "%2"; \
else \
py2_byte_compile "%1" "%2"; \
fi

View File

@ -1,74 +1,135 @@
# 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_sitearch %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_path('platlib', vars={'platbase': '%{_prefix}', 'base': '%{_prefix}'}))")
%python_version %(RPM_BUILD_ROOT= %{__python} -Esc "import sys; sys.stdout.write('{0.major}.{0.minor}'.format(sys.version_info))")
%python_version_nodots %(RPM_BUILD_ROOT= %{__python} -Esc "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))")
%python_platform %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_platform())")
%python_platform_triplet %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_config_var('MULTIARCH'))")
%python_ext_suffix %(RPM_BUILD_ROOT= %{__python} -Esc "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))")
%python_cache_tag %(RPM_BUILD_ROOT= %{__python} -Esc "import sys; print(sys.implementation.cache_tag)")
%py_setup setup.py
%py_shbang_opts -s
%_py_shebang_s s
%_py_shebang_P %(RPM_BUILD_ROOT= %{__python} -Esc "import sys; print('P' if hasattr(sys.flags, 'safe_path') else '')")
%py_shbang_opts -%{?_py_shebang_s}%{?_py_shebang_P}
%py_shbang_opts_nodash %(opts=%{py_shbang_opts}; echo ${opts#-})
%py_shebang_flags %(opts=%{py_shbang_opts}; echo ${opts#-})
%py_shebang_fix %{expand:\\\
if [ -f /usr/bin/pathfix%{python_version}.py ]; then
pathfix=/usr/bin/pathfix%{python_version}.py
else
# older versions of Python don't have it and must BR /usr/bin/pathfix.py from python3-devel explicitly
pathfix=/usr/bin/pathfix.py
fi
if [ -z "%{?py_shebang_flags}" ]; then
shebang_flags="-k"
else
shebang_flags="-ka%{py_shebang_flags}"
fi
$pathfix -pni %{__python} $shebang_flags}
# Use the slashes after expand so that the command starts on the same line as
# the macro
%py_build() %{expand:\\\
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python} %{py_setup} %{?py_setup_args} build --executable="%{__python2} %{py_shbang_opts}" %{?*}
sleep 1
%{__python} %{py_setup} %{?py_setup_args} build --executable="%{__python} %{py_shbang_opts}" %{?*}
}
%py_build_egg() %{expand:\\\
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python} %{py_setup} %{?py_setup_args} bdist_egg %{?*}
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() %{expand:\\\
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python} %{py_setup} %{?py_setup_args} install -O1 --skip-build --root %{buildroot} %{?*}
%{__python} %{py_setup} %{?py_setup_args} install -O1 --skip-build --root %{buildroot} --prefix %{_prefix} %{?*}
rm -rfv %{buildroot}%{_bindir}/__pycache__
}
%py_install_egg() %{expand:\\\
mkdir -p %{buildroot}%{python_sitelib}
easy_install -m --prefix %{buildroot}%{_prefix} -Z dist/*-py%{python_version}.egg %{?*}
%{__python} -m easy_install -m --prefix %{buildroot}%{_prefix} -Z dist/*-py%{python_version}.egg %{?*}
rm -rfv %{buildroot}%{_bindir}/__pycache__
}
%py_install_wheel() %{expand:\\\
pip install -I dist/%{1} --root %{buildroot} --strip-file-prefix %{buildroot} --no-deps
%{__python} -m pip install -I dist/%{1} --root %{buildroot} --prefix %{_prefix} --no-deps --no-index --no-warn-script-location
rm -rfv %{buildroot}%{_bindir}/__pycache__
for distinfo in %{buildroot}%{python_sitelib}/*.dist-info %{buildroot}%{python_sitearch}/*.dist-info; do
if [ -f ${distinfo}/direct_url.json ]; then
rm -fv ${distinfo}/direct_url.json
sed -i '/direct_url.json/d' ${distinfo}/RECORD
fi
done
}
# With $PATH and $PYTHONPATH set to the %%buildroot,
# try to import the Python module(s) given as command-line args or read from file (-f).
# Respect the custom values of %%py_shebang_flags or set nothing if it's undefined.
# Filter and check import on only top-level modules using -t flag.
# Exclude unwanted modules by passing their globs to -e option.
# Useful as a smoke test in %%check when running tests is not feasible.
# Use spaces or commas as separators if providing list directly.
# Use newlines as separators if providing list in a file.
%py_check_import(e:tf:) %{expand:\\\
PATH="%{buildroot}%{_bindir}:$PATH"\\\
PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python_sitearch}:%{buildroot}%{python_sitelib}}"\\\
_PYTHONSITE="%{buildroot}%{python_sitearch}:%{buildroot}%{python_sitelib}"\\\
PYTHONDONTWRITEBYTECODE=1\\\
%{lua:
local command = "%{__python} "
if rpm.expand("%{?py_shebang_flags}") ~= "" then
command = command .. "-%{py_shebang_flags}"
end
command = command .. " %{_rpmconfigdir}/redhat/import_all_modules.py "
-- handle multiline arguments correctly, see https://bugzilla.redhat.com/2018809
local args=rpm.expand('%{?**}'):gsub("[%s\\\\]*%s+", " ")
print(command .. args)
}
}
%python_provide() %{lua:
local python = require "fedora.srpm.python"
function string.starts(String,Start)
return string.sub(String,1,string.len(Start))==Start
end
package = rpm.expand("%{?1}")
vr = rpm.expand("%{?epoch:%{epoch}:}%{version}-%{release}")
if (string.starts(package, "python2-")) then
if (rpm.expand("%{?buildarch}") ~= "noarch") then
str = "Provides: python-" .. string.sub(package,9,string.len(package)) .. "%{?_isa} = " .. vr
print(rpm.expand(str))
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
print("\\nProvides: python-")
print(string.sub(package,9,string.len(package)))
print(" = ")
print(vr)
--Obsoleting the previous default python package
print("\\nObsoletes: python-")
print(string.sub(package,9,string.len(package)))
print(" < ")
print(vr)
elseif (string.starts(package, "python" .. rpm.expand("%{python3_pkgversion}") .. "-")) then
--No unversioned provides as python3 is not default
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, "pypy3-")) then
--No unversioned provides as pypy is not default
elseif (string.starts(package, "python-")) then
--Providing the current default python
print("Provides: python2-")
print(string.sub(package,8,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)
end
elseif (string.starts(package, "python" .. default_python3_pkgversion .. "-")) then
for i, provide in ipairs(provides) do
print("\\nProvides: " .. provide)
end
--Obsoleting the previous default python package (if it doesn't have isa)
if (string.sub(package, "-1") ~= ")") then
print("\\nObsoletes: python-")
print(string.sub(package,8+string.len(default_python3_pkgversion),string.len(package)))
print(" < " .. vr)
end
elseif (string.starts(package, "python")) then
--No unversioned provides as other python3 cases are not the default
elseif (string.starts(package, "pypy")) then
--No unversioned provides as pypy is not default either
else
print("%python_provide: ERROR: ")
print(package)
@ -76,6 +137,6 @@
end
}
%python_enable_dependency_generator() \
%global __python_requires %{_rpmconfigdir}/pythondistdeps.py --requires \
%python_disable_dependency_generator() \
%undefine __pythondist_requires \
%{nil}

View File

@ -1,12 +1,94 @@
# python3_pkgversion specifies the version of Python 3 in the distro. It can be
# a specific version (e.g. 34 in Fedora EPEL7)
# 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.11
#
# 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
# Set to /bin/true to avoid %ifdefs and %{? in specfiles
%__python3_other /bin/true
%py3_other_build /bin/true
%py3_other_install /bin/true
# Define the Python interpreter paths in the SRPM macros so that
# - they can be used in Build/Requires
# - they can be used in non-Python packages where requiring pythonX-devel would
# be an overkill
# use the underscored macros to redefine the behavior of %%python3_version etc.
%__python2 /usr/bin/python2
%__python3 /usr/bin/python%{python3_pkgversion}
# use the non-underscored macros to refer to Python in spec, etc.
%python2 %__python2
%python3 %__python3
# See https://fedoraproject.org/wiki/Changes/PythonMacroError
%__python %{error:attempt to use unversioned python, define %%__python to %{__python2} or %{__python3} explicitly}
# Users can use %%python only if they redefined %%__python (e.g. to %%__python3)
%python %__python
# Define where Python wheels will be stored and the prefix of -wheel packages
# - In Fedora we want wheel subpackages named e.g. `python-pip-wheel` that
# install packages into `/usr/share/python-wheels`. Both names are not
# versioned, because they're used by all Python 3 stacks.
# - In RHEL we want wheel packages named e.g. `python3-pip-wheel` and
# `python3.11-pip-wheel` that install packages into similarly versioned
# locations. We want each Python stack in RHEL to have their own wheels,
# because the main python3 wheels (which we can't upgrade) will likely be
# quite old by the time we're adding new alternate Python stacks.
# - In ELN we want to follow Fedora, because builds for ELN and Fedora rawhide
# need to be interoperable.
%python_wheel_pkg_prefix python%{?rhel:%{!?eln:%{python3_pkgversion}}}
%python_wheel_dir %{_datadir}/%{python_wheel_pkg_prefix}-wheels
### BRP scripts (and related macros)
## Automatically compile python files
%py_auto_byte_compile 1
## Should python bytecompilation errors terminate a build?
%_python_bytecompile_errors_terminate_build 1
## Should python bytecompilation compile outside python specific directories?
## This always causes errors when enabled, see https://fedoraproject.org/wiki/Changes/No_more_automagic_Python_bytecompilation_phase_3
%_python_bytecompile_extra 0
## The individual BRP scripts
%__brp_python_bytecompile %{_rpmconfigdir}/redhat/brp-python-bytecompile "" "%{?_python_bytecompile_errors_terminate_build}" "%{?_python_bytecompile_extra}"
%__brp_fix_pyc_reproducibility %{_rpmconfigdir}/redhat/brp-fix-pyc-reproducibility
%__brp_python_hardlink %{_rpmconfigdir}/redhat/brp-python-hardlink
## This macro is included in redhat-rpm-config's %%__os_install_post
# Note that the order matters:
# 1. brp-python-bytecompile can create (or replace) pyc files
# 2. brp-fix-pyc-reproducibility can modify the pyc files from above
# 3. brp-python-hardlink de-duplicates identical pyc files
%__os_install_post_python \
%{?py_auto_byte_compile:%{?__brp_python_bytecompile}} \
%{?py_reproducible_pyc_path:%{?__brp_fix_pyc_reproducibility} "%{py_reproducible_pyc_path}"} \
%{?__brp_python_hardlink} \
%{nil}
# === Macros for Build/Requires tags using Python dist tags ===
@ -17,13 +99,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\
@ -43,8 +125,9 @@
# 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\
@ -57,7 +140,142 @@
end\
for arg, name in ipairs(args) do\
canonical = rpm.expand("%py_dist_name " .. name);\
print("python3dist(" .. canonical .. ") ");\
print("python" .. python3_pkgversion .. "dist(" .. canonical .. ") ");\
end\
}
# Macro to replace overly complicated references to PyPI source files.
# Expands to the pythonhosted URL for a package
# Accepts zero to three arguments:
# 1: The PyPI project name, defaulting to %%srcname if it is defined, then
# %%pypi_name if it is defined, then just %%name.
# 2: The PYPI version, defaulting to %%version with tildes stripped.
# 3: The file extension, defaulting to "tar.gz". (A period will be added
# automatically.)
# Requires %%__pypi_url and %%__pypi_default_extension to be defined.
%__pypi_url https://files.pythonhosted.org/packages/source/
%__pypi_default_extension tar.gz
%pypi_source() %{lua:
local src = rpm.expand('%1')
local ver = rpm.expand('%2')
local ext = rpm.expand('%3')
local url = rpm.expand('%__pypi_url')
\
-- If no first argument, try %srcname, then %pypi_name, then %name
-- Note that rpm leaves macros unchanged if they are not defined.
if src == '%1' then
src = rpm.expand('%srcname')
end
if src == '%srcname' then
src = rpm.expand('%pypi_name')
end
if src == '%pypi_name' then
src = rpm.expand('%name')
end
\
-- If no second argument, use %version
if ver == '%2' then
ver = rpm.expand('%version'):gsub('~', '')
end
\
-- If no third argument, use the preset default extension
if ext == '%3' then
ext = rpm.expand('%__pypi_default_extension')
end
\
local first = string.sub(src, 1, 1)
\
print(url .. first .. '/' .. src .. '/' .. src .. '-' .. ver .. '.' .. ext)
}
%py_provides() %{lua:
local python = require 'fedora.srpm.python'
local rhel = rpm.expand('%{?rhel}')
local name = rpm.expand('%1')
if name == '%1' then
rpm.expand('%{error:%%py_provides requires at least 1 argument, the name to provide}')
end
local evr = rpm.expand('%2')
if evr == '%2' then
evr = rpm.expand('%{?epoch:%{epoch}:}%{version}-%{release}')
end
print('Provides: ' .. name .. ' = ' .. evr .. '\\n')
local provides = python.python_altprovides(name, evr)
for i, provide in ipairs(provides) do
print('Provides: ' .. provide .. '\\n')
end
-- We only generate these Obsoletes on CentOS/RHEL to provide clean upgrade
-- path, e.g. python3-foo obsoletes python3.9-foo from previous RHEL.
-- In Fedora this is not needed as we don't ship ecosystem packages
-- for alternative Python interpreters.
if rhel ~= '' then
-- Create Obsoletes only if the name does not end in a parenthesis,
-- as Obsoletes can't include parentheses.
-- This most commonly happens when the name contains an isa.
if (string.sub(name, "-1") ~= ")") then
local obsoletes = python.python_altobsoletes(name, evr)
for i, obsolete in ipairs(obsoletes) do
print('Obsoletes: ' .. obsolete .. '\\n')
end
end
end
}
%python_extras_subpkg(n:i:f:F) %{expand:%{lua:
local option_n = '-n (name of the base package)'
local option_i = '-i (buildroot path to metadata)'
local option_f = '-f (builddir path to a filelist)'
local option_F = '-F (skip %%files section)'
local value_n = rpm.expand('%{-n*}')
local value_i = rpm.expand('%{-i*}')
local value_f = rpm.expand('%{-f*}')
local value_F = rpm.expand('%{-F}')
local args = rpm.expand('%{*}')
if value_n == '' then
rpm.expand('%{error:%%%0: missing option ' .. option_n .. '}')
end
if value_i == '' and value_f == '' and value_F == '' then
rpm.expand('%{error:%%%0: missing option ' .. option_i .. ' or ' .. option_f .. ' or ' .. option_F .. '}')
end
if value_i ~= '' and value_f ~= '' then
rpm.expand('%{error:%%%0: simultaneous ' .. option_i .. ' and ' .. option_f .. ' options are not possible}')
end
if value_i ~= '' and value_F ~= '' then
rpm.expand('%{error:%%%0: simultaneous ' .. option_i .. ' and ' .. option_F .. ' options are not possible}')
end
if value_f ~= '' and value_F ~= '' then
rpm.expand('%{error:%%%0: simultaneous ' .. option_f .. ' and ' .. option_F .. ' options are not possible}')
end
if args == '' then
rpm.expand('%{error:%%%0 requires at least one argument with "extras" name}')
end
local requires = 'Requires: ' .. value_n .. ' = %{?epoch:%{epoch}:}%{version}-%{release}'
for extras in args:gmatch('[^%s,]+') do
local rpmname = value_n .. '+' .. extras
local pkgdef = '%package -n ' .. rpmname
local summary = 'Summary: Metapackage for ' .. value_n .. ': ' .. extras .. ' extras'
local description = '%description -n ' .. rpmname .. '\\\n'
local current_line = 'This is a metapackage bringing in'
for _, word in ipairs({extras, 'extras', 'requires', 'for', value_n .. '.'}) do
local line = current_line .. ' ' .. word
if line:len() > 79 then
description = description .. current_line .. '\\\n'
current_line = word
else
current_line = line
end
end
description = description .. current_line .. '\\\n' ..
'It makes sure the dependencies are installed.\\\n'
local files = ''
if value_i ~= '' then
files = '%files -n ' .. rpmname .. '\\\n' .. '%ghost ' .. value_i
elseif value_f ~= '' then
files = '%files -n ' .. rpmname .. ' -f ' .. value_f
end
for i, line in ipairs({pkgdef, summary, requires, description, files, ''}) do
print(line .. '\\\n')
end
end
}}

View File

@ -1,41 +0,0 @@
%__python2 /usr/bin/python2
%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
# Use the slashes after expand so that the command starts on the same line as
# the macro
%py2_build() %{expand:\\\
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:\\\
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python2} %{py_setup} %{?py_setup_args} bdist_egg %{?*}
sleep 1
}
%py2_build_wheel() %{expand:\\\
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} --strip-file-prefix %{buildroot} --no-deps
}

View File

@ -1,42 +1,122 @@
%__python3 /usr/bin/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(sys.version[:3])")
%python3_version_nodots %(%{__python3} -Ic "import sys; sys.stdout.write(sys.version[:3].replace('.',''))")
# 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} -Ic "import sysconfig; print(sysconfig.get_path('purelib', vars={'platbase': '%{_prefix}', 'base': '%{_prefix}'}))")
%python3_sitearch %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_path('platlib', vars={'platbase': '%{_prefix}', 'base': '%{_prefix}'}))")
%python3_version %(RPM_BUILD_ROOT= %{__python3} -Ic "import sys; sys.stdout.write('{0.major}.{0.minor}'.format(sys.version_info))")
%python3_version_nodots %(RPM_BUILD_ROOT= %{__python3} -Ic "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))")
%python3_platform %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_platform())")
%python3_platform_triplet %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_config_var('MULTIARCH'))")
%python3_ext_suffix %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))")
%python3_cache_tag %(RPM_BUILD_ROOT= %{__python3} -Ic "import sys; print(sys.implementation.cache_tag)")
%py3dir %{_builddir}/python3-%{name}-%{version}-%{release}
%py3_shbang_opts -s
%_py3_shebang_s s
%_py3_shebang_P %(RPM_BUILD_ROOT= %{__python3} -Ic "import sys; print('P' if hasattr(sys.flags, 'safe_path') else '')")
%py3_shbang_opts -%{?_py3_shebang_s}%{?_py3_shebang_P}
%py3_shbang_opts_nodash %(opts=%{py3_shbang_opts}; echo ${opts#-})
%py3_shebang_flags %(opts=%{py3_shbang_opts}; echo ${opts#-})
%py3_shebang_fix %{expand:\\\
if [ -f /usr/bin/pathfix%{python3_version}.py ]; then
pathfix=/usr/bin/pathfix%{python3_version}.py
else
# older versions of Python don't have it and must BR /usr/bin/pathfix.py from python3-devel explicitly
pathfix=/usr/bin/pathfix.py
fi
if [ -z "%{?py3_shebang_flags}" ]; then
shebang_flags="-k"
else
shebang_flags="-ka%{py3_shebang_flags}"
fi
$pathfix -pni %{__python3} $shebang_flags}
# Use the slashes after expand so that the command starts on the same line as
# the macro
%py3_build() %{expand:\\\
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python3} %{py_setup} %{?py_setup_args} build --executable="%{__python3} %{py3_shbang_opts}" %{?*}
sleep 1
}
%py3_build_egg() %{expand:\\\
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python3} %{py_setup} %{?py_setup_args} bdist_egg %{?*}
sleep 1
}
%py3_build_wheel() %{expand:\\\
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
%{__python3} %{py_setup} %{?py_setup_args} bdist_wheel %{?*}
sleep 1
}
%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} %{?*}
%{__python3} %{py_setup} %{?py_setup_args} install -O1 --skip-build --root %{buildroot} --prefix %{_prefix} %{?*}
rm -rfv %{buildroot}%{_bindir}/__pycache__
}
%py3_install_egg() %{expand:\\\
mkdir -p %{buildroot}%{python3_sitelib}
easy_install-%{python3_version} -m --prefix %{buildroot}%{_prefix} -Z dist/*-py%{python3_version}.egg %{?*}
%{__python3} -m easy_install -m --prefix %{buildroot}%{_prefix} -Z dist/*-py%{python3_version}.egg %{?*}
rm -rfv %{buildroot}%{_bindir}/__pycache__
}
%py3_install_wheel() %{expand:\\\
pip%{python3_version} install -I dist/%{1} --root %{buildroot} --strip-file-prefix %{buildroot} --no-deps
%{__python3} -m pip install -I dist/%{1} --root %{buildroot} --prefix %{_prefix} --no-deps --no-index --no-warn-script-location
rm -rfv %{buildroot}%{_bindir}/__pycache__
for distinfo in %{buildroot}%{python3_sitelib}/*.dist-info %{buildroot}%{python3_sitearch}/*.dist-info; do
if [ -f ${distinfo}/direct_url.json ]; then
rm -fv ${distinfo}/direct_url.json
sed -i '/direct_url.json/d' ${distinfo}/RECORD
fi
done
}
# With $PATH and $PYTHONPATH set to the %%buildroot,
# try to import the Python 3 module(s) given as command-line args or read from file (-f).
# Respect the custom values of %%py3_shebang_flags or set nothing if it's undefined.
# Filter and check import on only top-level modules using -t flag.
# Exclude unwanted modules by passing their globs to -e option.
# Useful as a smoke test in %%check when running tests is not feasible.
# Use spaces or commas as separators if providing list directly.
# Use newlines as separators if providing list in a file.
%py3_check_import(e:tf:) %{expand:\\\
PATH="%{buildroot}%{_bindir}:$PATH"\\\
PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}}"\\\
_PYTHONSITE="%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}"\\\
PYTHONDONTWRITEBYTECODE=1\\\
%{lua:
local command = "%{__python3} "
if rpm.expand("%{?py3_shebang_flags}") ~= "" then
command = command .. "-%{py3_shebang_flags}"
end
command = command .. " %{_rpmconfigdir}/redhat/import_all_modules.py "
-- handle multiline arguments correctly, see https://bugzilla.redhat.com/2018809
local args=rpm.expand('%{?**}'):gsub("[%s\\\\]*%s+", " ")
print(command .. args)
}
}
# This only supports Python 3.5+ and will never work with Python 2.
# Hence, it has no Python version in the name.
%pycached() %{lua:
path = rpm.expand("%{?*}")
if (string.sub(path, "-3") ~= ".py") then
rpm.expand("%{error:%%pycached can only be used with paths explicitly ending with .py}")
else
print(path)
pyminor = path:match("/python3.(%d+)/") or "*"
dirname = path:match("(.*/)")
modulename = path:match(".*/([^/]+).py")
-- %%python3_cache_tag is not used here because this macro supports not-installed CPythons
print("\\n" .. dirname .. "__pycache__/" .. modulename .. ".cpython-3" .. pyminor .. "{,.opt-?}.pyc")
end
}
# This is intended for Python 3 only, hence also no Python version in the name.
%__pytest /usr/bin/pytest%(test %{python3_pkgversion} == 3 || echo -%{python3_version})
%pytest %{expand:\\\
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
PATH="%{buildroot}%{_bindir}:$PATH"\\\
PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}}"\\\
PYTHONDONTWRITEBYTECODE=1\\\
%{?__pytest_addopts:PYTEST_ADDOPTS="${PYTEST_ADDOPTS:-} %{__pytest_addopts}"}\\\
%__pytest}

View File

@ -1,19 +1,68 @@
Name: python-rpm-macros
Version: 3
Release: 28%{?dist}
Summary: The unversioned Python RPM macros
Summary: The common Python RPM macros
License: MIT
Source0: macros.python
Source1: macros.python-srpm
Source2: macros.python2
Source3: macros.python3
URL: https://src.fedoraproject.org/rpms/python-rpm-macros/
# Macros:
Source101: macros.python
Source102: macros.python-srpm
Source104: macros.python3
Source105: macros.pybytecompile
# Lua files
Source201: python.lua
# Python code
%global compileall2_version 0.7.1
Source301: https://github.com/fedora-python/compileall2/raw/v%{compileall2_version}/compileall2.py
Source302: import_all_modules.py
# BRP scripts
# This one is from redhat-rpm-config < 190
# A new upstream is forming in https://github.com/rpm-software-management/python-rpm-packaging/blob/main/scripts/brp-python-bytecompile
# But our version is riddled with Fedora-isms
# We might eventually move to upstream source + Fedora patches, but we are not there yet
Source401: brp-python-bytecompile
# This one is from https://github.com/rpm-software-management/python-rpm-packaging/blob/main/scripts/brp-python-hardlink
# But we don't use a link in case it changes in upstream, there are no "versions" there yet
# This was removed from RPM 4.17+ so we maintain it here instead
Source402: brp-python-hardlink
# This one is from redhat-rpm-config < 190
# It has no upstream yet
Source403: brp-fix-pyc-reproducibility
# macros and lua: MIT
# import_all_modules.py: MIT
# compileall2.py: PSFv2
# brp scripts: GPLv2+
License: MIT and Python and GPLv2+
# 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: 4%{?dist}
BuildArch: noarch
# For %%python3_pkgversion used in %%python_provide
Requires: python-srpm-macros
Obsoletes: python-macros < 3
Provides: python-macros = %{version}-%{release}
# For %%__default_python3_pkgversion used in %%python_provide
# For python.lua
# For compileall2.py
Requires: python-srpm-macros = %{version}-%{release}
# The packages are called python(3)-(s)rpm-macros
# We never want python3-rpm-macros to provide python-rpm-macros
# We opt out from all Python name-based automatic provides and obsoletes
%undefine __pythonname_provides
%undefine __pythonname_obsoletes
%description
This package contains the unversioned Python RPM macros, that most
@ -22,53 +71,361 @@ implementations should rely on.
You should not need to install this package manually as the various
python?-devel packages require it. So install a python-devel package instead.
%package -n python-srpm-macros
Summary: RPM macros for building Python source packages
# For directory structure and flags macros
# Versions before 190 contained some brp scripts moved into python-srpm-macros
Requires: redhat-rpm-config >= 190
# We bundle our own software here :/
Provides: bundled(python3dist(compileall2)) = %{compileall2_version}
%description -n python-srpm-macros
RPM macros for building Python source packages.
%package -n python2-rpm-macros
Summary: RPM macros for building Python 2 packages
# 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
# Would need to be different for each release - worth it?
#Conflicts: python3-devel < 3.5.1-3
# For %%__python3 and %%python3
Requires: python-srpm-macros = %{version}-%{release}
# For %%py_setup and import_all_modules.py
Requires: python-rpm-macros = %{version}-%{release}
%description -n python3-rpm-macros
RPM macros for building Python 3 packages.
%prep
%autosetup -c -T
cp -a %{sources} .
%build
%install
mkdir -p %{buildroot}/%{rpmmacrodir}
install -m 644 %{SOURCE0} %{SOURCE1} %{SOURCE2} %{SOURCE3} \
%{buildroot}/%{rpmmacrodir}/
mkdir -p %{buildroot}%{rpmmacrodir}
install -m 644 macros.* %{buildroot}%{rpmmacrodir}/
mkdir -p %{buildroot}%{_rpmluadir}/fedora/srpm
install -p -m 644 -t %{buildroot}%{_rpmluadir}/fedora/srpm python.lua
mkdir -p %{buildroot}%{_rpmconfigdir}/redhat
install -m 644 compileall2.py %{buildroot}%{_rpmconfigdir}/redhat/
install -m 644 import_all_modules.py %{buildroot}%{_rpmconfigdir}/redhat/
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
%global __brp_python_bytecompile %{buildroot}%{__brp_python_bytecompile}
%global __brp_python_hardlink %{buildroot}%{__brp_python_hardlink}
%global __brp_fix_pyc_reproducibility %{buildroot}%{__brp_fix_pyc_reproducibility}
%check
# no macros in comments
grep -E '^#[^%%]*%%[^%%]' %{buildroot}%{rpmmacrodir}/macros.* && exit 1 || true
%files
%{rpmmacrodir}/macros.python
%{rpmmacrodir}/macros.pybytecompile
%{_rpmconfigdir}/redhat/import_all_modules.py
%files -n python-srpm-macros
%{rpmmacrodir}/macros.python-srpm
%files -n python2-rpm-macros
%{rpmmacrodir}/macros.python2
%{_rpmconfigdir}/redhat/compileall2.py
%{_rpmconfigdir}/redhat/brp-python-bytecompile
%{_rpmconfigdir}/redhat/brp-python-hardlink
%{_rpmconfigdir}/redhat/brp-fix-pyc-reproducibility
%{_rpmluadir}/fedora/srpm/python.lua
%files -n python3-rpm-macros
%{rpmmacrodir}/macros.python3
%changelog
* Fri 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
- 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
- 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
- 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
- Make pythonX-rpm-macros depend on python-rpm-macros (#1827811)
* Tue Mar 31 2020 Lumír Balhar <lbalhar@redhat.com> - 3.8-2
- 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
- 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
- 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
* Fri Sep 27 2019 Miro Hrončok <mhroncok@redhat.com> - 3-49
- Define %%python2 and %%python3
* Mon Aug 26 2019 Miro Hrončok <mhroncok@redhat.com> - 3-48
- Drop --strip-file-prefix option from %%pyX_install_wheel macros, it is not needed
* Fri Jul 26 2019 Fedora Release Engineering <releng@fedoraproject.org> - 3-47
- Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild
* Fri Jul 12 2019 Miro Hrončok <mhroncok@redhat.com> - 3-46
- %%python_provide: Switch python2 and python3 behavior
- https://fedoraproject.org/wiki/Changes/Python_means_Python3
- Use compileall2 module for byte-compilation with Python >= 3.4
- Do not allow passing arguments to Python during byte-compilation
- Use `-s` argument for Python during byte-compilation
* Tue Jul 09 2019 Miro Hrončok <mhroncok@redhat.com> - 3-45
- %%python_provide: Don't try to obsolete %%_isa provides
* Mon Jun 17 2019 Miro Hrončok <mhroncok@redhat.com> - 3-44
- Make %%__python /usr/bin/python once again until we are ready
* Mon Jun 10 2019 Miro Hrončok <mhroncok@redhat.com> - 3-43
- Define %%python_sitelib, %%python_sitearch, %%python_version, %%python_version_nodots,
in rpm 4.15 those are no longer defined, the meaning of python is derived from %%__python.
- Usage of %%__python or the above-mentioned macros will error unless user defined.
- The %%python_provide macro no longer gives the arched provide for arched packages (#1705656)
* Sat Feb 02 2019 Fedora Release Engineering <releng@fedoraproject.org> - 3-42
- Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild
* Thu Dec 20 2018 Igor Gnatenko <ignatenkobrain@fedoraproject.org> - 3-41
- 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)
* Thu Nov 01 2018 Petr Viktorin <pviktori@redhat.com> - 3-39
- Move "sleep 1" workaround from py3_build to py2_build (#1644923)
* Thu Sep 20 2018 Tomas Orsava <torsava@redhat.com> - 3-38
- Move the __python2/3 macros to the python-srpm-macros subpackage
- This facilitates using the %%{__python2/3} in Build/Requires
* Wed Aug 15 2018 Miro Hrončok <mhroncok@redhat.com> - 3-37
- Make %%py_byte_compile terminate build on SyntaxErrors (#1616219)
* Wed Aug 15 2018 Miro Hrončok <mhroncok@redhat.com> - 3-36
- Make %%py_build wokr if %%__python is defined to custom value
* Sat Jul 28 2018 Igor Gnatenko <ignatenkobrain@fedoraproject.org> - 3-35
- Change way how enabling-depgen works internally
* Sat Jul 14 2018 Tomas Orsava <torsava@redhat.com> - 3-34
- macros.pybytecompile: Detect Python version through sys.version_info instead
of guessing from the executable name
* Sat Jul 14 2018 Fedora Release Engineering <releng@fedoraproject.org> - 3-33
- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild
* Tue Jul 10 2018 Tomas Orsava <torsava@redhat.com> - 3-32
- Fix %%py_byte_compile macro: when invoked with a Python 2 binary it also
mistakenly ran py3_byte_compile
* Tue Jul 03 2018 Miro Hrončok <mhroncok@redhat.com> - 3-31
- Add %%python3_platform useful for PYTHONPATH on arched builds
* Mon Jun 18 2018 Jason L Tibbitts III <tibbs@math.uh.edu> - 3-30
- Add %%pypi_source macro, as well as %%__pypi_url and
%%_pypi_default_extension.
* Wed Apr 18 2018 Miro Hrončok <mhroncok@redhat.com> - 3-29
- move macros.pybytecompile from python3-devel
* Fri Apr 06 2018 Tomas Orsava <torsava@redhat.com> - 3-28
- Fix the %%py_dist_name macro to not convert dots (".") into dashes, so that
submodules can be addressed as well

101
python.lua Normal file
View File

@ -0,0 +1,101 @@
-- Convenience Lua functions that can be used within Python srpm/rpm macros
-- Determine alternate names provided from the given name.
-- Used in pythonname provides generator, python_provide and py_provides.
-- If only_3_to_3_X is false/nil/unused there are 2 rules:
-- python3-foo -> python-foo, python3.X-foo
-- python3.X-foo -> python-foo, python3-foo
-- If only_3_to_3_X is true there is only 1 rule:
-- python3-foo -> python3.X-foo
-- There is no python-foo -> rule, python-foo packages are version agnostic.
-- Returns a table/array with strings. Empty when no rule matched.
local function python_altnames(name, only_3_to_3_X)
local xy = rpm.expand('%{__default_python3_pkgversion}')
local altnames = {}
local replaced
-- NB: dash needs to be escaped!
if name:match('^python3%-') then
local prefixes = only_3_to_3_X and {} or {'python-'}
for i, prefix in ipairs({'python' .. xy .. '-', table.unpack(prefixes)}) do
replaced = name:gsub('^python3%-', prefix)
table.insert(altnames, replaced)
end
elseif name:match('^python' .. xy .. '%-') and not only_3_to_3_X then
for i, prefix in ipairs({'python-', 'python3-'}) do
replaced = name:gsub('^python' .. xy .. '%-', prefix)
table.insert(altnames, replaced)
end
end
return altnames
end
local function __python_alttags(name, evr, tag_type)
-- for the "provides" tag_type we want also unversioned provides
local only_3_to_3_X = tag_type ~= "provides"
local operator = tag_type == "provides" and ' = ' or ' < '
-- global cache that tells what package NEVRs were already processed for the
-- given tag type
if __python_alttags_beenthere == nil then
__python_alttags_beenthere = {}
end
if __python_alttags_beenthere[tag_type] == nil then
__python_alttags_beenthere[tag_type] = {}
end
__python_alttags_beenthere[tag_type][name .. ' ' .. evr] = true
local alttags = {}
for i, altname in ipairs(python_altnames(name, only_3_to_3_X)) do
table.insert(alttags, altname .. operator .. evr)
end
return alttags
end
-- For any given name and epoch-version-release, return provides except self.
-- Uses python_altnames under the hood
-- Returns a table/array with strings.
local function python_altprovides(name, evr)
return __python_alttags(name, evr, "provides")
end
-- For any given name and epoch-version-release, return versioned obsoletes except self.
-- Uses python_altnames under the hood
-- Returns a table/array with strings.
local function python_altobsoletes(name, evr)
return __python_alttags(name, evr, "obsoletes")
end
local function __python_alttags_once(name, evr, tag_type)
-- global cache that tells what provides were already processed
if __python_alttags_beenthere == nil
or __python_alttags_beenthere[tag_type] == nil
or __python_alttags_beenthere[tag_type][name .. ' ' .. evr] == nil then
return __python_alttags(name, evr, tag_type)
else
return nil
end
end
-- Like python_altprovides but only return something once.
-- For each argument can only be used once, returns nil otherwise.
-- Previous usage of python_altprovides counts as well.
local function python_altprovides_once(name, evr)
return __python_alttags_once(name, evr, "provides")
end
-- Like python_altobsoletes but only return something once.
-- For each argument can only be used once, returns nil otherwise.
-- Previous usage of python_altobsoletes counts as well.
local function python_altobsoletes_once(name, evr)
return __python_alttags_once(name, evr, "obsoletes")
end
return {
python_altnames = python_altnames,
python_altprovides = python_altprovides,
python_altobsoletes = python_altobsoletes,
python_altprovides_once = python_altprovides_once,
python_altobsoletes_once = python_altobsoletes_once,
}

7
rpminspect.yaml Normal file
View File

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

1
tests/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__*__/

73
tests/pythontest.spec Normal file
View File

@ -0,0 +1,73 @@
%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.
%global python36_sitelib /usr/lib/python3.6/site-packages
%global python27_sitelib /usr/lib/python2.7/site-packages
Name: pythontest
Version: 0
Release: 0
Summary: ...
License: MIT
BuildRequires: python3-devel
BuildRequires: python3.6
BuildRequires: python2.7
%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
mkdir -p %{buildroot}%{python27_sitelib}/directory/
echo "print()" > %{buildroot}%{python27_sitelib}/directory/file.py
%check
LOCATIONS="
%{buildroot}%{basedir}
%{buildroot}%{python3_sitelib}/directory/
%{buildroot}%{python36_sitelib}/directory/
%{buildroot}%{python27_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 5 .py files (3 for python3, one each for 3.6 & 2.7)
test $PY -eq 5
# 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
%{python27_sitelib}/directory/file.py*

854
tests/test_evals.py Normal file
View File

@ -0,0 +1,854 @@
import os
import subprocess
import platform
import re
import sys
import textwrap
import pytest
X_Y = f'{sys.version_info[0]}.{sys.version_info[1]}'
XY = f'{sys.version_info[0]}{sys.version_info[1]}'
# Handy environment variable you can use to run the tests
# with modified macros files. Multiple files should be
# separated by colon.
# You can use * if you escape it from your Shell:
# TESTED_FILES='macros.*' pytest -v
# Remember that some tests might need more macros files than just
# the local ones. You might need to use:
# TESTED_FILES='/usr/lib/rpm/macros:/usr/lib/rpm/platform/x86_64-linux/macros:macros.*'
TESTED_FILES = os.getenv("TESTED_FILES", None)
def rpm_eval(expression, fails=False, **kwargs):
cmd = ['rpmbuild']
if TESTED_FILES:
cmd += ['--macros', TESTED_FILES]
for var, value in kwargs.items():
if value is None:
cmd += ['--undefine', var]
else:
cmd += ['--define', f'{var} {value}']
cmd += ['--eval', expression]
cp = subprocess.run(cmd, text=True, env={**os.environ, 'LANG': 'C.utf-8'},
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if fails:
assert cp.returncode != 0, cp.stdout
elif fails is not None:
assert cp.returncode == 0, cp.stdout
return cp.stdout.strip().splitlines()
@pytest.fixture(scope="session")
def lib():
lib_eval = rpm_eval("%_lib")[0]
if lib_eval == "%_lib" and TESTED_FILES:
raise ValueError(
"%_lib is not resolved to an actual value. "
"You may want to include /usr/lib/rpm/platform/x86_64-linux/macros to TESTED_FILES."
)
return lib_eval
def get_alt_x_y():
"""
Some tests require alternate Python version to be installed.
In order to allow any Python version (or none at all),
this function/fixture exists.
You can control the behavior by setting the $ALTERNATE_PYTHON_VERSION
environment variable to X.Y (e.g. 3.6) or SKIP.
The environment variable must be set.
"""
env_name = "ALTERNATE_PYTHON_VERSION"
alternate_python_version = os.getenv(env_name, "")
if alternate_python_version.upper() == "SKIP":
pytest.skip(f"${env_name} set to SKIP")
if not alternate_python_version:
raise ValueError(f"${env_name} must be set, "
f"set it to SKIP if you want to skip tests that "
f"require alternate Python version.")
if not re.match(r"^\d+\.\d+$", alternate_python_version):
raise ValueError(f"${env_name} must be X.Y")
return alternate_python_version
def get_alt_xy():
"""
Same as get_alt_x_y() but without a dot
"""
return get_alt_x_y().replace(".", "")
# We don't use decorators, to be able to call the functions directly
alt_x_y = pytest.fixture(scope="session")(get_alt_x_y)
alt_xy = pytest.fixture(scope="session")(get_alt_xy)
# https://fedoraproject.org/wiki/Changes/PythonSafePath
def safe_path_flag(x_y):
return 'P' if tuple(int(i) for i in x_y.split('.')) >= (3, 11) else ''
def shell_stdout(script):
return subprocess.check_output(script,
env={**os.environ, 'LANG': 'C.utf-8'},
text=True,
shell=True).rstrip()
@pytest.mark.parametrize('macro', ['%__python3', '%python3'])
def test_python3(macro):
assert rpm_eval(macro) == ['/usr/bin/python3']
@pytest.mark.parametrize('macro', ['%__python3', '%python3'])
@pytest.mark.parametrize('pkgversion', ['3', '3.9', '3.12'])
def test_python3_with_pkgversion(macro, pkgversion):
assert rpm_eval(macro, python3_pkgversion=pkgversion) == [f'/usr/bin/python{pkgversion}']
@pytest.mark.parametrize('argument, result', [
('a', 'a'),
('a-a', 'a-a'),
('a_a', 'a-a'),
('a.a', 'a-a'),
('a---a', 'a-a'),
('a-_-a', 'a-a'),
('a-_-a', 'a-a'),
('a[b]', 'a[b]'),
('Aha[Boom]', 'aha[boom]'),
('a.a[b.b]', 'a-a[b-b]'),
])
def test_pydist_name(argument, result):
assert rpm_eval(f'%py_dist_name {argument}') == [result]
def test_py2_dist():
assert rpm_eval(f'%py2_dist Aha[Boom] a') == ['python2dist(aha[boom]) python2dist(a)']
def test_py3_dist():
assert rpm_eval(f'%py3_dist Aha[Boom] a') == ['python3dist(aha[boom]) python3dist(a)']
def test_py3_dist_with_python3_pkgversion_redefined(alt_x_y):
assert rpm_eval(f'%py3_dist Aha[Boom] a', python3_pkgversion=alt_x_y) == [f'python{alt_x_y}dist(aha[boom]) python{alt_x_y}dist(a)']
def test_python_provide_python():
assert rpm_eval('%python_provide python-foo') == []
def test_python_provide_python3():
lines = rpm_eval('%python_provide python3-foo', version='6', release='1.fc66')
assert 'Obsoletes: python-foo < 6-1.fc66' in lines
assert 'Provides: python-foo = 6-1.fc66' in lines
assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines
assert len(lines) == 3
def test_python_provide_python3_epoched():
lines = rpm_eval('%python_provide python3-foo', epoch='1', version='6', release='1.fc66')
assert 'Obsoletes: python-foo < 1:6-1.fc66' in lines
assert 'Provides: python-foo = 1:6-1.fc66' in lines
assert f'Provides: python{X_Y}-foo = 1:6-1.fc66' in lines
assert len(lines) == 3
def test_python_provide_python3X():
lines = rpm_eval(f'%python_provide python{X_Y}-foo', version='6', release='1.fc66')
assert 'Obsoletes: python-foo < 6-1.fc66' in lines
assert 'Provides: python-foo = 6-1.fc66' in lines
assert 'Provides: python3-foo = 6-1.fc66' in lines
assert len(lines) == 3
def test_python_provide_python3X_epoched():
lines = rpm_eval(f'%python_provide python{X_Y}-foo', epoch='1', version='6', release='1.fc66')
assert 'Obsoletes: python-foo < 1:6-1.fc66' in lines
assert 'Provides: python-foo = 1:6-1.fc66' in lines
assert 'Provides: python3-foo = 1:6-1.fc66' in lines
assert len(lines) == 3
def test_python_provide_doubleuse():
lines = rpm_eval('%{python_provide python3-foo}%{python_provide python3-foo}',
version='6', release='1.fc66')
assert 'Obsoletes: python-foo < 6-1.fc66' in lines
assert 'Provides: python-foo = 6-1.fc66' in lines
assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines
assert len(lines) == 6
assert len(set(lines)) == 3
@pytest.mark.parametrize('rhel', [None, 10])
def test_py_provides_python(rhel):
lines = rpm_eval('%py_provides python-foo', version='6', release='1.fc66', rhel=rhel)
assert 'Provides: python-foo = 6-1.fc66' in lines
assert len(lines) == 1
@pytest.mark.parametrize('rhel', [None, 12])
def test_py_provides_whatever(rhel):
lines = rpm_eval('%py_provides whatever', version='6', release='1.fc66', rhel=rhel)
assert 'Provides: whatever = 6-1.fc66' in lines
assert len(lines) == 1
@pytest.mark.parametrize('rhel', [None, 9])
def test_py_provides_python3(rhel):
lines = rpm_eval('%py_provides python3-foo', version='6', release='1.fc66', rhel=rhel)
assert 'Provides: python3-foo = 6-1.fc66' in lines
assert 'Provides: python-foo = 6-1.fc66' in lines
assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines
if rhel:
assert f'Obsoletes: python{X_Y}-foo < 6-1.fc66' in lines
assert len(lines) == 4
else:
assert len(lines) == 3
@pytest.mark.parametrize('rhel', [None, 9])
def test_py_provides_python3_with_isa(rhel):
lines = rpm_eval('%py_provides python3-foo(x86_64)', version='6', release='1.fc66', rhel=rhel)
assert 'Provides: python3-foo(x86_64) = 6-1.fc66' in lines
assert 'Provides: python-foo(x86_64) = 6-1.fc66' in lines
assert f'Provides: python{X_Y}-foo(x86_64) = 6-1.fc66' in lines
assert f'Obsoletes: python{X_Y}-foo(x86_64) < 6-1.fc66' not in lines
assert len(lines) == 3
@pytest.mark.parametrize('rhel', [None, 13])
def test_py_provides_python3_epoched(rhel):
lines = rpm_eval('%py_provides python3-foo', epoch='1', version='6', release='1.fc66', rhel=rhel)
assert 'Provides: python3-foo = 1:6-1.fc66' in lines
assert 'Provides: python-foo = 1:6-1.fc66' in lines
assert f'Provides: python{X_Y}-foo = 1:6-1.fc66' in lines
if rhel:
assert f'Obsoletes: python{X_Y}-foo < 1:6-1.fc66' in lines
assert len(lines) == 4
else:
assert len(lines) == 3
@pytest.mark.parametrize('rhel', [None, 13])
def test_py_provides_python3X(rhel):
lines = rpm_eval(f'%py_provides python{X_Y}-foo', version='6', release='1.fc66', rhel=rhel)
assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines
assert 'Provides: python-foo = 6-1.fc66' in lines
assert 'Provides: python3-foo = 6-1.fc66' in lines
assert len(lines) == 3
@pytest.mark.parametrize('rhel', [None, 27])
def test_py_provides_python3X_epoched(rhel):
lines = rpm_eval(f'%py_provides python{X_Y}-foo', epoch='1', version='6', release='1.fc66', rhel=rhel)
assert f'Provides: python{X_Y}-foo = 1:6-1.fc66' in lines
assert 'Provides: python-foo = 1:6-1.fc66' in lines
assert 'Provides: python3-foo = 1:6-1.fc66' in lines
assert len(lines) == 3
@pytest.mark.parametrize('rhel', [None, 2])
def test_py_provides_doubleuse(rhel):
lines = rpm_eval('%{py_provides python3-foo}%{py_provides python3-foo}',
version='6', release='1.fc66', rhel=rhel)
assert 'Provides: python3-foo = 6-1.fc66' in lines
assert 'Provides: python-foo = 6-1.fc66' in lines
assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines
if rhel:
assert f'Obsoletes: python{X_Y}-foo < 6-1.fc66' in lines
assert len(lines) == 8
assert len(set(lines)) == 4
else:
assert len(lines) == 6
assert len(set(lines)) == 3
@pytest.mark.parametrize('rhel', [None, 2])
def test_py_provides_with_evr(rhel):
lines = rpm_eval('%py_provides python3-foo 123',
version='6', release='1.fc66', rhel=rhel)
assert 'Provides: python3-foo = 123' in lines
assert 'Provides: python-foo = 123' in lines
assert f'Provides: python{X_Y}-foo = 123' in lines
if rhel:
assert f'Obsoletes: python{X_Y}-foo < 123' in lines
assert len(lines) == 4
else:
assert len(lines) == 3
def test_python_wheel_pkg_prefix():
assert rpm_eval('%python_wheel_pkg_prefix', fedora='44', rhel=None, eln=None) == ['python']
assert rpm_eval('%python_wheel_pkg_prefix', fedora='44', rhel=None, eln=None, python3_pkgversion='3.9') == ['python']
assert rpm_eval('%python_wheel_pkg_prefix', fedora=None, rhel='1', eln='1') == ['python']
assert rpm_eval('%python_wheel_pkg_prefix', fedora=None, rhel='1', eln=None) == ['python3']
assert rpm_eval('%python_wheel_pkg_prefix', fedora=None, rhel='1', eln=None, python3_pkgversion='3.10') == ['python3.10']
assert rpm_eval('%python_wheel_pkg_prefix', fedora=None, rhel='1', eln=None, python3_pkgversion='3.11') == ['python3.11']
def test_python_wheel_dir():
assert rpm_eval('%python_wheel_dir', fedora='44', rhel=None, eln=None) == ['/usr/share/python-wheels']
assert rpm_eval('%python_wheel_dir', fedora='44', rhel=None, eln=None, python3_pkgversion='3.9') == ['/usr/share/python-wheels']
assert rpm_eval('%python_wheel_dir', fedora=None, rhel='1', eln='1') == ['/usr/share/python-wheels']
assert rpm_eval('%python_wheel_dir', fedora=None, rhel='1', eln=None) == ['/usr/share/python3-wheels']
assert rpm_eval('%python_wheel_dir', fedora=None, rhel='1', eln=None, python3_pkgversion='3.10') == ['/usr/share/python3.10-wheels']
assert rpm_eval('%python_wheel_dir', fedora=None, rhel='1', eln=None, python3_pkgversion='3.11') == ['/usr/share/python3.11-wheels']
def test_pytest_passes_options_naturally():
lines = rpm_eval('%pytest -k foo')
assert '/usr/bin/pytest -k foo' in lines[-1]
def test_pytest_different_command():
lines = rpm_eval('%pytest', __pytest='pytest-3')
assert 'pytest-3' in lines[-1]
def test_pytest_command_suffix():
lines = rpm_eval('%pytest -v')
assert '/usr/bin/pytest -v' in lines[-1]
# this test does not require alternate Pythons to be installed
@pytest.mark.parametrize('version', ['3.6', '3.7', '3.12'])
def test_pytest_command_suffix_alternate_pkgversion(version):
lines = rpm_eval('%pytest -v', python3_pkgversion=version, python3_version=version)
assert f'/usr/bin/pytest-{version} -v' in lines[-1]
def test_pytest_undefined_addopts_are_not_set():
lines = rpm_eval('%pytest', __pytest_addopts=None)
assert 'PYTEST_ADDOPTS' not in '\n'.join(lines)
def test_pytest_defined_addopts_are_set():
lines = rpm_eval('%pytest', __pytest_addopts="--ignore=stuff")
assert 'PYTEST_ADDOPTS="${PYTEST_ADDOPTS:-} --ignore=stuff"' in '\n'.join(lines)
@pytest.mark.parametrize('__pytest_addopts', ['--macronized-option', 'x y z', None])
def test_pytest_addopts_preserves_envvar(__pytest_addopts):
# this is the line a packager might put in the spec file before running %pytest:
spec_line = 'export PYTEST_ADDOPTS="--exported-option1 --exported-option2"'
# instead of actually running /usr/bin/pytest,
# we run a small shell script that echoes the tested value for inspection
lines = rpm_eval('%pytest', __pytest_addopts=__pytest_addopts,
__pytest="sh -c 'echo $PYTEST_ADDOPTS'")
echoed = shell_stdout('\n'.join([spec_line] + lines))
# assert all values were echoed
assert '--exported-option1' in echoed
assert '--exported-option2' in echoed
if __pytest_addopts is not None:
assert __pytest_addopts in echoed
# assert the options are separated
assert 'option--' not in echoed
assert 'z--' not in echoed
def test_pypi_source_default_name():
urls = rpm_eval('%pypi_source',
name='foo', version='6')
assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz']
def test_pypi_source_default_srcname():
urls = rpm_eval('%pypi_source',
name='python-foo', srcname='foo', version='6')
assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz']
def test_pypi_source_default_pypi_name():
urls = rpm_eval('%pypi_source',
name='python-foo', pypi_name='foo', version='6')
assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz']
def test_pypi_source_default_name_uppercase():
urls = rpm_eval('%pypi_source',
name='Foo', version='6')
assert urls == ['https://files.pythonhosted.org/packages/source/F/Foo/Foo-6.tar.gz']
def test_pypi_source_provided_name():
urls = rpm_eval('%pypi_source foo',
name='python-bar', pypi_name='bar', version='6')
assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz']
def test_pypi_source_provided_name_version():
urls = rpm_eval('%pypi_source foo 6',
name='python-bar', pypi_name='bar', version='3')
assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz']
def test_pypi_source_provided_name_version_ext():
url = rpm_eval('%pypi_source foo 6 zip',
name='python-bar', pypi_name='bar', version='3')
assert url == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.zip']
def test_pypi_source_prerelease():
urls = rpm_eval('%pypi_source',
name='python-foo', pypi_name='foo', version='6~b2')
assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6b2.tar.gz']
def test_pypi_source_explicit_tilde():
urls = rpm_eval('%pypi_source foo 6~6',
name='python-foo', pypi_name='foo', version='6')
assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6~6.tar.gz']
def test_py3_shebang_fix():
cmd = rpm_eval('%py3_shebang_fix arg1 arg2 arg3')[-1].strip()
assert cmd == '$pathfix -pni /usr/bin/python3 $shebang_flags arg1 arg2 arg3'
def test_py3_shebang_fix_default_shebang_flags():
lines = rpm_eval('%py3_shebang_fix arg1 arg2')
lines[-1] = 'echo $shebang_flags'
assert shell_stdout('\n'.join(lines)) == f'-kas{safe_path_flag(X_Y)}'
def test_py3_shebang_fix_custom_shebang_flags():
lines = rpm_eval('%py3_shebang_fix arg1 arg2', py3_shebang_flags='Es')
lines[-1] = 'echo $shebang_flags'
assert shell_stdout('\n'.join(lines)) == '-kaEs'
@pytest.mark.parametrize('_py3_shebang_s', [None, '%{nil}'])
def test_py3_shebang_fix_undefined_py3_shebang_s(_py3_shebang_s):
lines = rpm_eval('%py3_shebang_fix arg1 arg2', _py3_shebang_s=_py3_shebang_s)
lines[-1] = 'echo $shebang_flags'
expected = f'-ka{safe_path_flag(X_Y)}' if safe_path_flag(X_Y) else '-k'
assert shell_stdout('\n'.join(lines)) == expected
@pytest.mark.parametrize('_py3_shebang_P', [None, '%{nil}'])
def test_py3_shebang_fix_undefined_py3_shebang_P(_py3_shebang_P):
lines = rpm_eval('%py3_shebang_fix arg1 arg2', _py3_shebang_P=_py3_shebang_P)
lines[-1] = 'echo $shebang_flags'
assert shell_stdout('\n'.join(lines)) == '-kas'
@pytest.mark.parametrize('_py3_shebang_s', [None, '%{nil}'])
@pytest.mark.parametrize('_py3_shebang_P', [None, '%{nil}'])
def test_py3_shebang_fix_undefined_py3_shebang_sP(_py3_shebang_s, _py3_shebang_P):
lines = rpm_eval('%py3_shebang_fix arg1 arg2',
_py3_shebang_s=_py3_shebang_s,
_py3_shebang_P=_py3_shebang_P)
lines[-1] = 'echo $shebang_flags'
assert shell_stdout('\n'.join(lines)) == '-k'
@pytest.mark.parametrize('flags', [None, '%{nil}'])
def test_py3_shebang_fix_no_shebang_flags(flags):
lines = rpm_eval('%py3_shebang_fix arg1 arg2', py3_shebang_flags=flags)
lines[-1] = 'echo $shebang_flags'
assert shell_stdout('\n'.join(lines)) == '-k'
def test_py_shebang_fix_custom_python():
cmd = rpm_eval('%py_shebang_fix arg1 arg2 arg3', __python='/usr/bin/pypy')[-1].strip()
assert cmd == '$pathfix -pni /usr/bin/pypy $shebang_flags arg1 arg2 arg3'
def test_pycached_in_sitelib():
lines = rpm_eval('%pycached %{python3_sitelib}/foo*.py')
assert lines == [
f'/usr/lib/python{X_Y}/site-packages/foo*.py',
f'/usr/lib/python{X_Y}/site-packages/__pycache__/foo*.cpython-{XY}{{,.opt-?}}.pyc'
]
def test_pycached_in_sitearch(lib):
lines = rpm_eval('%pycached %{python3_sitearch}/foo*.py')
assert lines == [
f'/usr/{lib}/python{X_Y}/site-packages/foo*.py',
f'/usr/{lib}/python{X_Y}/site-packages/__pycache__/foo*.cpython-{XY}{{,.opt-?}}.pyc'
]
# this test does not require alternate Pythons to be installed
@pytest.mark.parametrize('version', ['3.6', '3.7', '3.12'])
def test_pycached_with_alternate_version(version):
version_nodot = version.replace('.', '')
lines = rpm_eval(f'%pycached /usr/lib/python{version}/site-packages/foo*.py')
assert lines == [
f'/usr/lib/python{version}/site-packages/foo*.py',
f'/usr/lib/python{version}/site-packages/__pycache__/foo*.cpython-{version_nodot}{{,.opt-?}}.pyc'
]
def test_pycached_in_custom_dir():
lines = rpm_eval('%pycached /bar/foo*.py')
assert lines == [
'/bar/foo*.py',
'/bar/__pycache__/foo*.cpython-3*{,.opt-?}.pyc'
]
def test_pycached_with_exclude():
lines = rpm_eval('%pycached %exclude %{python3_sitelib}/foo*.py')
assert lines == [
f'%exclude /usr/lib/python{X_Y}/site-packages/foo*.py',
f'%exclude /usr/lib/python{X_Y}/site-packages/__pycache__/foo*.cpython-{XY}{{,.opt-?}}.pyc'
]
def test_pycached_fails_with_extension_glob():
lines = rpm_eval('%pycached %{python3_sitelib}/foo.py*', fails=True)
assert lines[0] == 'error: %pycached can only be used with paths explicitly ending with .py'
def test_python_extras_subpkg_i():
lines = rpm_eval('%python_extras_subpkg -n python3-setuptools_scm -i %{python3_sitelib}/*.egg-info toml yaml',
version='6', release='7')
expected = textwrap.dedent(f"""
%package -n python3-setuptools_scm+toml
Summary: Metapackage for python3-setuptools_scm: toml extras
Requires: python3-setuptools_scm = 6-7
%description -n python3-setuptools_scm+toml
This is a metapackage bringing in toml extras requires for
python3-setuptools_scm.
It makes sure the dependencies are installed.
%files -n python3-setuptools_scm+toml
%ghost /usr/lib/python{X_Y}/site-packages/*.egg-info
%package -n python3-setuptools_scm+yaml
Summary: Metapackage for python3-setuptools_scm: yaml extras
Requires: python3-setuptools_scm = 6-7
%description -n python3-setuptools_scm+yaml
This is a metapackage bringing in yaml extras requires for
python3-setuptools_scm.
It makes sure the dependencies are installed.
%files -n python3-setuptools_scm+yaml
%ghost /usr/lib/python{X_Y}/site-packages/*.egg-info
""").lstrip().splitlines()
assert lines == expected
def test_python_extras_subpkg_f():
lines = rpm_eval('%python_extras_subpkg -n python3-setuptools_scm -f ghost_filelist toml yaml',
version='6', release='7')
expected = textwrap.dedent(f"""
%package -n python3-setuptools_scm+toml
Summary: Metapackage for python3-setuptools_scm: toml extras
Requires: python3-setuptools_scm = 6-7
%description -n python3-setuptools_scm+toml
This is a metapackage bringing in toml extras requires for
python3-setuptools_scm.
It makes sure the dependencies are installed.
%files -n python3-setuptools_scm+toml -f ghost_filelist
%package -n python3-setuptools_scm+yaml
Summary: Metapackage for python3-setuptools_scm: yaml extras
Requires: python3-setuptools_scm = 6-7
%description -n python3-setuptools_scm+yaml
This is a metapackage bringing in yaml extras requires for
python3-setuptools_scm.
It makes sure the dependencies are installed.
%files -n python3-setuptools_scm+yaml -f ghost_filelist
""").lstrip().splitlines()
assert lines == expected
def test_python_extras_subpkg_F():
lines = rpm_eval('%python_extras_subpkg -n python3-setuptools_scm -F toml yaml',
version='6', release='7')
expected = textwrap.dedent(f"""
%package -n python3-setuptools_scm+toml
Summary: Metapackage for python3-setuptools_scm: toml extras
Requires: python3-setuptools_scm = 6-7
%description -n python3-setuptools_scm+toml
This is a metapackage bringing in toml extras requires for
python3-setuptools_scm.
It makes sure the dependencies are installed.
%package -n python3-setuptools_scm+yaml
Summary: Metapackage for python3-setuptools_scm: yaml extras
Requires: python3-setuptools_scm = 6-7
%description -n python3-setuptools_scm+yaml
This is a metapackage bringing in yaml extras requires for
python3-setuptools_scm.
It makes sure the dependencies are installed.
""").lstrip().splitlines()
assert lines == expected
def test_python_extras_subpkg_underscores():
lines = rpm_eval('%python_extras_subpkg -n python3-webscrapbook -F adhoc_ssl',
version='0.33.3', release='1.fc33')
expected = textwrap.dedent(f"""
%package -n python3-webscrapbook+adhoc_ssl
Summary: Metapackage for python3-webscrapbook: adhoc_ssl extras
Requires: python3-webscrapbook = 0.33.3-1.fc33
%description -n python3-webscrapbook+adhoc_ssl
This is a metapackage bringing in adhoc_ssl extras requires for
python3-webscrapbook.
It makes sure the dependencies are installed.
""").lstrip().splitlines()
assert lines == expected
@pytest.mark.parametrize('sep', [pytest.param(('', ' ', ' ', ''), id='spaces'),
pytest.param(('', ',', ',', ''), id='commas'),
pytest.param(('', ',', ',', ','), id='commas-trailing'),
pytest.param((',', ',', ',', ''), id='commas-leading'),
pytest.param((',', ',', ',', ','), id='commas-trailing-leading'),
pytest.param(('', ',', ' ', ''), id='mixture'),
pytest.param((' ', ' ', '\t\t, ', '\t'), id='chaotic-good'),
pytest.param(('', '\t ,, \t\r ', ',,\t , ', ',,'), id='chaotic-evil')])
def test_python_extras_subpkg_arg_separators(sep):
lines = rpm_eval('%python_extras_subpkg -n python3-hypothesis -F {}cli{}ghostwriter{}pytz{}'.format(*sep),
version='6.6.0', release='1.fc35')
expected = textwrap.dedent(f"""
%package -n python3-hypothesis+cli
Summary: Metapackage for python3-hypothesis: cli extras
Requires: python3-hypothesis = 6.6.0-1.fc35
%description -n python3-hypothesis+cli
This is a metapackage bringing in cli extras requires for python3-hypothesis.
It makes sure the dependencies are installed.
%package -n python3-hypothesis+ghostwriter
Summary: Metapackage for python3-hypothesis: ghostwriter extras
Requires: python3-hypothesis = 6.6.0-1.fc35
%description -n python3-hypothesis+ghostwriter
This is a metapackage bringing in ghostwriter extras requires for
python3-hypothesis.
It makes sure the dependencies are installed.
%package -n python3-hypothesis+pytz
Summary: Metapackage for python3-hypothesis: pytz extras
Requires: python3-hypothesis = 6.6.0-1.fc35
%description -n python3-hypothesis+pytz
This is a metapackage bringing in pytz extras requires for python3-hypothesis.
It makes sure the dependencies are installed.
""").lstrip().splitlines()
assert lines == expected
@pytest.mark.parametrize('basename_len', [1, 10, 30, 45, 78])
@pytest.mark.parametrize('extra_len', [1, 13, 28, 52, 78])
def test_python_extras_subpkg_description_wrapping(basename_len, extra_len):
basename = 'x' * basename_len
extra = 'y' * extra_len
lines = rpm_eval(f'%python_extras_subpkg -n {basename} -F {extra}',
version='6', release='7')
for idx, line in enumerate(lines):
if line.startswith('%description'):
start = idx + 1
lines = lines[start:]
assert all(len(l) < 80 for l in lines)
assert len(lines) < 6
if len(" ".join(lines[:-1])) < 80:
assert len(lines) == 2
expected_singleline = (f"This is a metapackage bringing in {extra} extras "
f"requires for {basename}. "
f"It makes sure the dependencies are installed.")
description_singleline = " ".join(lines)
assert description_singleline == expected_singleline
unversioned_macros = pytest.mark.parametrize('macro', [
'%__python',
'%python',
'%python_version',
'%python_version_nodots',
'%python_sitelib',
'%python_sitearch',
'%python_platform',
'%python_platform_triplet',
'%python_ext_suffix',
'%python_cache_tag',
'%py_shebang_fix',
'%py_build',
'%py_build_egg',
'%py_build_wheel',
'%py_install',
'%py_install_egg',
'%py_install_wheel',
'%py_check_import',
])
@unversioned_macros
def test_unversioned_python_errors(macro):
lines = rpm_eval(macro, fails=True)
assert lines == ['error: attempt to use unversioned python, '
'define %__python to /usr/bin/python2 or /usr/bin/python3 explicitly']
@unversioned_macros
def test_unversioned_python_works_when_defined(macro):
macro3 = macro.replace('python', 'python3').replace('py_', 'py3_')
assert rpm_eval(macro, __python='/usr/bin/python3') == rpm_eval(macro3)
# we could rework the test for multiple architectures, but the Fedora CI currently only runs on x86_64
x86_64_only = pytest.mark.skipif(platform.machine() != "x86_64", reason="works on x86_64 only")
@x86_64_only
def test_platform_triplet():
assert rpm_eval("%python3_platform_triplet") == ["x86_64-linux-gnu"]
@x86_64_only
def test_ext_suffix():
assert rpm_eval("%python3_ext_suffix") == [f".cpython-{XY}-x86_64-linux-gnu.so"]
def test_cache_tag():
assert rpm_eval("%python3_cache_tag") == [f"cpython-{XY}"]
def test_cache_tag_alternate_python(alt_x_y, alt_xy):
assert rpm_eval("%python_cache_tag", __python=f"/usr/bin/python{alt_x_y}") == [f"cpython-{alt_xy}"]
def test_cache_tag_alternate_python3(alt_x_y, alt_xy):
assert rpm_eval("%python3_cache_tag", __python3=f"/usr/bin/python{alt_x_y}") == [f"cpython-{alt_xy}"]
def test_python_sitelib_value_python3():
macro = '%python_sitelib'
assert rpm_eval(macro, __python='%__python3') == [f'/usr/lib/python{X_Y}/site-packages']
def test_python_sitelib_value_alternate_python(alt_x_y):
macro = '%python_sitelib'
assert rpm_eval(macro, __python=f'/usr/bin/python{alt_x_y}') == [f'/usr/lib/python{alt_x_y}/site-packages']
def test_python3_sitelib_value_default():
macro = '%python3_sitelib'
assert rpm_eval(macro) == [f'/usr/lib/python{X_Y}/site-packages']
def test_python3_sitelib_value_alternate_python(alt_x_y):
macro = '%python3_sitelib'
assert (rpm_eval(macro, __python3=f'/usr/bin/python{alt_x_y}') ==
rpm_eval(macro, python3_pkgversion=alt_x_y) ==
[f'/usr/lib/python{alt_x_y}/site-packages'])
def test_python3_sitelib_value_alternate_prefix():
macro = '%python3_sitelib'
assert rpm_eval(macro, _prefix='/app') == [f'/app/lib/python{X_Y}/site-packages']
def test_python_sitearch_value_python3(lib):
macro = '%python_sitearch'
assert rpm_eval(macro, __python='%__python3') == [f'/usr/{lib}/python{X_Y}/site-packages']
def test_python_sitearch_value_alternate_python(lib, alt_x_y):
macro = '%python_sitearch'
assert rpm_eval(macro, __python=f'/usr/bin/python{alt_x_y}') == [f'/usr/{lib}/python{alt_x_y}/site-packages']
def test_python3_sitearch_value_default(lib):
macro = '%python3_sitearch'
assert rpm_eval(macro) == [f'/usr/{lib}/python{X_Y}/site-packages']
def test_python3_sitearch_value_alternate_python(lib, alt_x_y):
macro = '%python3_sitearch'
assert (rpm_eval(macro, __python3=f'/usr/bin/python{alt_x_y}') ==
rpm_eval(macro, python3_pkgversion=alt_x_y) ==
[f'/usr/{lib}/python{alt_x_y}/site-packages'])
def test_python3_sitearch_value_alternate_prefix(lib):
macro = '%python3_sitearch'
assert rpm_eval(macro, _prefix='/app') == [f'/app/{lib}/python{X_Y}/site-packages']
@pytest.mark.parametrize(
'args, expected_args',
[
('six', 'six'),
('-f foo.txt', '-f foo.txt'),
('-t -f foo.txt six, seven', '-t -f foo.txt six, seven'),
('-e "foo*" -f foo.txt six, seven', '-e "foo*" -f foo.txt six, seven'),
('six.quarter six.half,, SIX', 'six.quarter six.half,, SIX'),
('-f foo.txt six\nsix.half\nSIX', '-f foo.txt six six.half SIX'),
('six \\ -e six.half', 'six -e six.half'),
]
)
@pytest.mark.parametrize('__python3',
[None,
f'/usr/bin/python{X_Y}',
'/usr/bin/pythonX.Y'])
def test_py3_check_import(args, expected_args, __python3, lib):
x_y = X_Y
macros = {
'buildroot': 'BUILDROOT',
'_rpmconfigdir': 'RPMCONFIGDIR',
}
if __python3 is not None:
if 'X.Y' in __python3:
__python3 = __python3.replace('X.Y', get_alt_x_y())
macros['__python3'] = __python3
# If the __python3 command has version at the end, parse it and expect it.
# Note that the command is used to determine %python3_sitelib and %python3_sitearch,
# so we only test known CPython schemes here and not PyPy for simplicity.
if (match := re.match(r'.+python(\d+\.\d+)$', __python3)):
x_y = match.group(1)
invocation = '%{py3_check_import ' + args +'}'
lines = rpm_eval(invocation, **macros)
# An equality check is a bit inflexible here,
# every time we change the macro we need to change this test.
# However actually executing it and verifying the result is much harder :/
# At least, let's make the lines saner to check:
lines = [line.rstrip('\\').strip() for line in lines]
expected = textwrap.dedent(fr"""
PATH="BUILDROOT/usr/bin:$PATH"
PYTHONPATH="${{PYTHONPATH:-BUILDROOT/usr/{lib}/python{x_y}/site-packages:BUILDROOT/usr/lib/python{x_y}/site-packages}}"
_PYTHONSITE="BUILDROOT/usr/{lib}/python{x_y}/site-packages:BUILDROOT/usr/lib/python{x_y}/site-packages"
PYTHONDONTWRITEBYTECODE=1
{__python3 or '/usr/bin/python3'} -s{safe_path_flag(x_y)} RPMCONFIGDIR/redhat/import_all_modules.py {expected_args}
""")
assert lines == expected.splitlines()
@pytest.mark.parametrize(
'shebang_flags_value, expected_shebang_flags',
[
('sP', '-sP'),
('s', '-s'),
('%{nil}', ''),
(None, ''),
('Es', '-Es'),
]
)
def test_py3_check_import_respects_shebang_flags(shebang_flags_value, expected_shebang_flags, lib):
macros = {
'_rpmconfigdir': 'RPMCONFIGDIR',
'__python3': '/usr/bin/python3',
'py3_shebang_flags': shebang_flags_value,
}
lines = rpm_eval('%py3_check_import sys', **macros)
# Compare the last line of the command, that's where lua part is evaluated
expected = f'/usr/bin/python3 {expected_shebang_flags} RPMCONFIGDIR/redhat/import_all_modules.py sys'
assert lines[-1].strip() == expected

View File

@ -0,0 +1,426 @@
from import_all_modules import argparser, exclude_unwanted_module_globs
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(ModuleNotFoundError):
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(ModuleNotFoundError):
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\nsunau\n')
# Make sure the tested modules are not already in sys.modules
for m in ('math', 'wave', 'sunau'):
sys.modules.pop(m, None)
modules_main(['-f', str(test_file)])
assert 'sunau' 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('sunau\npathlib\n')
test_file_3.write_text('logging\nsunau\n')
# Make sure the tested modules are not already in sys.modules
for m in ('math', 'wave', 'sunau', 'pathlib', '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 ('sunau', 'math', 'wave', 'pathlib', '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(ModuleNotFoundError):
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(ModuleNotFoundError):
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(ModuleNotFoundError):
modules_main([
'this_is_a_module_in_tmp_path_1',
'this_is_a_module_in_tmp_path_2',
])
def test_module_with_error_propagates_exception(tmp_path):
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))
# The correct exception must be raised
with pytest.raises(ZeroDivisionError):
modules_main([
'this_is_a_module_in_tmp_path_1',
])
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'

30
tests/tests.yml Normal file
View File

@ -0,0 +1,30 @@
---
- hosts: localhost
tags:
- classic
tasks:
- dnf:
name: "*"
state: latest
- hosts: localhost
roles:
- role: standard-test-basic
tags:
- classic
tests:
- pytest:
dir: .
run: PYTHONPATH=/usr/lib/rpm/redhat ALTERNATE_PYTHON_VERSION=3.6 pytest -v
- manual_byte_compilation:
dir: .
run: rpmbuild -ba pythontest.spec
required_packages:
- rpm-build
- python-rpm-macros
- python3-rpm-macros
- python3-devel
- python3-pytest
- python3.6
- python2.7