├── .gitignore ├── LICENSE ├── README.md ├── apply-macros.sh ├── compile-macros.sh ├── default-prjconf ├── flavor.in ├── functions.lua ├── macros-default-pythons ├── macros.in ├── macros.lua ├── macros ├── 001-alternatives ├── 010-common-defs └── 030-fallbacks └── testfiles └── fix-shebang.spec /.gitignore: -------------------------------------------------------------------------------- 1 | /macros.python_all 2 | /macros/020-flavor-* 3 | /macros/035-default-pythons 4 | /macros/040-buildset 5 | /macros/041-buildset 6 | /macros/040-builset-start 7 | /macros/042-builset-end 8 | /macros/050-automagic 9 | *.swp 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE HELL YOU WANT TO PUBLIC LICENSE 2 | Version 1.0.0, October 2014 3 | 4 | DO WHAT THE HELL YOU WANT TO PUBLIC LICENSE 5 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 6 | 7 | 0. You just DO WHAT THE HELL YOU WANT TO. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi-Python, Single-Spec Macro System 2 | 3 | This repository contains a work-in-progress macro system generator for the singlespec Python initiative. 4 | The macro system can be used in spec files for building RPM packages. 5 | 6 | The purpose of the singlespec system is to take a package for a particular flavor, and 7 | autogenerate subpackages for all the other flavors. 8 | 9 | 10 | ### Terminology 11 | 12 | __````__ is a kind of python interpreter. At this point, we recognize the following flavors: 13 | `python2`, `python3`, `python38`, `python39`, `python310`, `python311` and `pypy3`. `python3` points to the default of 14 | coinstallable flavors `python3` where `` is the minor version number. The default is 15 | specified not by python-rpm-macros but by the obs project definition in `%primary_python`. 16 | 17 | The flavor is used as a prefix for all flavor-specific macros. 18 | Some macros are redefined with "short" flavor for compatibility 19 | reasons, such as `py3` for `python3`. All of them have a "long" form too. 20 | 21 | For compatibility reasons you see sometimes `python`. In most places, 22 | using `python` is either a redefinition of `python2`, or an alternative for 23 | "flavor-agnostic". Conditionals are in place to switch `python` to mean `python3` in the future. 24 | 25 | The name of the binary in `%_bindir` (`/usr/bin`) is the name of the flavor with an addtional `.` 26 | between the major and minor version number, in case the latter is part of the flavor name: 27 | 28 | - `/usr/bin/python2` 29 | - `/usr/bin/python3` 30 | - `/usr/bin/python3.8` 31 | - `/usr/bin/python3.10` 32 | - ... 33 | 34 | __modname__ is the PyPI name, or, if the package in question is not on PyPI, the moniker that we 35 | chose to stand in for it. 36 | 37 | Packages adhering to the SUSE Python module naming policy are usually called `-modname`. 38 | In some cases, it is only `modname` though. 39 | 40 | __pkgname__, or __subpackage name__, is internal to a spec file, and is that thing you put after 41 | the `%package` macro. Pkgname of the package itself is an empty string. Pkgname of a 42 | `%package -n something` is at this point `-n something`, and denotes that this subpackage should 43 | not be handled by the generator. That means, if you want a subpackage to be skipped, rename it 44 | from `%package foo` to `%package -n %{name}-foo`. 45 | 46 | The purpose of the singlespec system is to take a package called `-modname` for a 47 | particular flavor, and autogenerate subpackages for all the other flavors. 48 | 49 | Alternately, it is to take package `python-modname` and generate subpackages for all flavors, 50 | leaving the top-level package empty. 51 | 52 | Additionally it is possible for non-Python packages which define a subpackage 53 | `%package -n python-modname` and corresponding `%description -n python-modname` etc., 54 | to autogenerate all desired flavor subpackages `-modname`. 55 | 56 | ### Build Set 57 | 58 | The default build set is listed in the __`%pythons`__ macro. Every entry in `%pythons` generates a 59 | requirement in `%python_module`, a subpackage from `%python_subpackages` (unless the top-level spec 60 | file is for that flavor), and an additional run of loops like `%python_build`, `_install`, `_exec` 61 | and `_expand`. 62 | 63 | To control the build set, you can either completely redefine `%pythons`, or exclude 64 | particular flavor(s) by defining __`%skip_`__. For example, if you `%define skip_python2 1`, 65 | then Python 2 will be excluded from the default build set. (Python 2 is not in the default 66 | build set of Tumbleweed and SLE/Leap >= 15.4) 67 | 68 | Skip-macros are intended __for per-package use only__. Never define a skip-macro in prjconf or 69 | in any other sort of global config. Instead, redefine `%pythons`. 70 | 71 | ### Macros 72 | 73 | The following macros are considered public API: 74 | 75 | * __`%system_python`__ - flavor that is used for generic unflavored `%python_` macros. 76 | Currently set to `python2`. 77 | 78 | * __`%python_for_executables`__ - flavor that is used for installing executables into `%_bindir` and 79 | other files in non-flavor-specific locations. By default, set to `python3`. 80 | 81 | * __`%pythons`__ - the build set. See above for details. 82 | 83 | * __`%have_`__. Defined as 1 if the flavor is present in the build environment. 84 | Undefined otherwise. 85 | 86 | _Note:_ "present in build environment" does not mean "part of build set". Under some 87 | circumstances, you can get a Python flavor pulled in through dependencies, even if you exclude it 88 | from the build set. In such case, `%have_` will be defined but packages will not be 89 | generated for it. 90 | 91 | * __`%skip_`__. Undefined by default. Define in order to exclude a flavor from build set. 92 | 93 | _Note:_ You do not need to define `%skip_python2` for Tumbleweed. Only define, if you need to skip it 94 | for older distributions. 95 | 96 | * __`%{python_module modname args}`__ expands to `(-modname args)` for every 97 | flavor. Intended as: `BuildRequires: %{python_module foo >= version}`. Supports 98 | [rpm boolean dependencies](https://rpm.org/user_doc/boolean_dependencies.html). 99 | If the package needs a module only for a specific Python version, you can use the special pseudo-macro 100 | `%python` for expansion of the python-flavor within the requirement, e.g. 101 | `BuildRequires: %{python_module python-aiocontextvars >= 0.2.2 if %python-base < 3.7}`. 102 | (Don't define `%python` anywhere else.) 103 | 104 | * __`%{python_dist_name modname}`__. Given a standardized name (i.e. dist name, name on PyPI) of `modname`, 105 | it will convert it to a canonical format. 106 | 107 | * __`%{python2_dist modname}`__. Given a standardized name (i.e. dist name, name on PyPI) of `modname`, 108 | it will convert it to a canonical format, and evaluates to python2.Ydist(CANONICAL_NAME), which is useful 109 | when listing dependencies. Intended as `(Build)Requires: %{python2_dist foo}`. 110 | 111 | * __`%{python3_dist modname}`__. Given a standardized name (i.e. dist name, name on PyPI) of `modname`, 112 | it will convert it to a canonical format, and evaluates to python3.Ydist(CANONICAL_NAME), which is useful 113 | when listing dependencies. Intended as `(Build)Requires: %{python3_dist foo}`. 114 | 115 | * __`%python_flavor`__ expands to the `%pythons` entry that is currently being processed. 116 | Does not apply in `%prep`, `%build`, `%install` and `%check` sections. For those, check for the 117 | pseudo-shell variable expansion of `$python` and `$python_flavor` inside the `%python_expand` macro 118 | (see [Flavor expansion](#flavor-expansion)). 119 | 120 | * __`%python_subpackages`__ expands to the autogenerated subpackages. This should go at the end of the 121 | main headers section. 122 | 123 | * __`%python_subpackage_only`__. Undefined by default. If you want to generate `-modname` 124 | subpackages for a non-python main package, make sure to `%define python_subpackage_only 1` before 125 | `%python_subpackages` and use `-n python-modname` for section headers (except for `%files`, see below). 126 | 127 | * __`%python_enable_dependency_generator`__ expands to a define to enable automatic requires generation 128 | of Python module dependencies using egg-info/dist-info metadata. This should go above the 129 | `%python_subpackages` macro, preferably closer to the top of the spec. Intended usage: 130 | `%{?python_enable_dependency_generator}`. This macro will eventually be removed when the generator 131 | is configured to automatically run, hence the `?` at the beginning of the macro invocation. 132 | 133 | 134 | #### Conditionals 135 | 136 | These are shortcuts for `%if "%python_flavor" == ""`. Due to how RPM evaluates the 137 | shortcuts, they will fail when nested with other `%if` conditions. If you need to nest your 138 | conditions, use the full `%if "%python_flavor"` spelling. 139 | 140 | * __`%if`__: applies the following section only to subpackages of that particular flavor. 141 | 142 | * __`%ifpycache`__: applies the following section only to subpackages of flavors that generate a 143 | `__pycache__` directory. 144 | 145 | * __`%_only`__: applies the contents of the line only to subpackages of that particular flavor. 146 | 147 | * __`%pycache_only`__: applies the contents of the line only to subpackages of flavors that generate 148 | `__pycache__` directories. Useful in filelists: `%pycache_only %{python_sitelib}/__pycache__/*` 149 | 150 | 151 | #### Flavor expansion 152 | 153 | The following macros expand to command lists for all flavors and move around the distutils-generated 154 | `build` directory so that you are never running a `python39` command with a python310-generated `build` 155 | and vice versa. 156 | 157 | ##### General command expansion macros 158 | 159 | * __`%python_exec something.py`__ expands to `$python something.py` for all flavors, where `$python` 160 | is the basename of the flavor executable. Make sure it is in `$PATH`. 161 | 162 | * __`%python_expand something`__ is a more general form of the above. It performs rpm macro expansion 163 | of its arguments for every flavor. Importantly, `$python` is not expanded by the shell, but replaced 164 | beforehand for the current flavor, even in macros: 165 | 166 | - When used as command delimited by space or one of `"'\)&|;<>`, it is replaced by the path to the executable. 167 | - When used as part of a macro name or other string, it is replaced by the current flavor name. 168 | 169 | So: 170 | `%python_expand $python generatefile.py %{$python_sitelib}` 171 | expands to: 172 | 173 | ``` 174 | python3.8 generatefile.py /usr/lib/python3.8/site-packages 175 | python3.9 generatefile.py /usr/lib/python3.9/site-packages 176 | python3.10 generatefile.py /usr/lib/python3.10/site-packages 177 | ``` 178 | 179 | etc. (plus the moving around of the `build` directory in between). 180 | 181 | If you want to check for the current python flavor inside `%python_expand`, either use the shell variale 182 | `${python_flavor}` (not `$python_flavor`, `%{python_flavor}` or `%{$python_flavor}`), or append a suffix, 183 | which is not one of the recognized delimiters listed above: 184 | 185 | ```spec 186 | %{python_expand # expanded-body: 187 | if [ ${python_flavor} = python310 ]; then 188 | $python command-for-py-310-only 189 | fi 190 | echo "We have version %{$python_version}, because we are in $python_flavor." 191 | echo "Cannot use %{$python_flavor} because it has not enough levels of expansion." 192 | echo "And %{python_flavor} is expanded early to the global default." 193 | if [ $python_ = python310_ ]; then 194 | echo "A suffix_ works as intended." 195 | fi 196 | } 197 | ``` 198 | 199 | which expands during the python39 flavor iteration to 200 | 201 | ```sh 202 | # (.. moving build dirs ..) 203 | python_flavor=python39 204 | 205 | # expanded-body: 206 | if [ ${python_flavor} = python310 ]; then 207 | python3.9 command-for-py-310-only 208 | fi 209 | echo "We have version 3.9, because we are in python39_flavor." 210 | echo "Cannot use %{python39_flavor} because it has not enough levels of expansion." 211 | echo "And python310 is expanded early to the global default." 212 | if [ python39_ = python310_ ]; then 213 | echo "A suffix_ works as intended." 214 | fi 215 | ``` 216 | 217 | and so on for all flavors. 218 | 219 | ##### Install macros 220 | 221 | * __`%pyproject_wheel`__ expands to 222 | [PEP517](https://www.python.org/dev/peps/pep-0517)/[PEP518](https://www.python.org/dev/peps/pep-0518/) 223 | build instructions for all flavors, creates wheels and places them into the flavor's `./build/` directories 224 | (specified by `%_pyproject_wheeldir`). In case of pure wheels only one wheel is created by the first flavor, 225 | placed into `./dist/` (`%_pyproject_anywheeldir`) and copied over to `%_pyproject_wheeldir` for all other 226 | flavors. 227 | 228 | * __`%pyproject_install [wheelfile]`__ expands to install instructions for all flavors to install the created wheels. 229 | You can also use this without `%pyproject_wheel`, if you place a pre-existing wheel into the current working dir 230 | (deprecated), the `build/` directory of the current flavor (what `%pyproject_wheel` does), or specify 231 | the path to the wheel file explicitly as argument to the macro (preferred), e.g `%pyproject_install %{SOURCE0}`. 232 | 233 | * __`%python_compileall`__ precompiles all python source files in `%{python_sitelib}` and `%{python_sitearch}` 234 | for all flavors. Generally Python 2 creates the cached byte-code `.pyc` files directly in the script directories, while 235 | newer flavors generate `__pycache__` directories. Use this if you have modified the source files in `%buildroot` after 236 | `%python_install` or `%pyproject_install` has compiled the files the first time. 237 | 238 | * __`%python_build`__ expands to distutils/setuptools build instructions for all flavors using `setup.py`. 239 | 240 | * __`%python_install`__ expands to legacy distutils/setuptools install instructions for all flavors using `setup.py`. 241 | Note that `python setup.py install` has been deprecated by setuptools and distutils is deprecated entirely. 242 | Consider using the PEP517 install procedure using the `%pyproject_*` macros if the package sources support it. 243 | 244 | * __`%python_clone filename`__ creates a copy of `filename` under a flavor-specific name for every 245 | flavor. This is useful for packages that install unversioned executables: `/usr/bin/foo` is copied 246 | to `/usr/bin/foo-%{python_bin_suffix}` for all flavors, and the shebang is modified accordingly. 247 | __`%python_clone -a filename`__ will also invoke __`%prepare_alternative`__ with the appropriate 248 | arguments or create the libalternative configuration if `--with libalternatives` is specified. 249 | 250 | * __`%python_find_lang foo`__ calls `%find_lang foo` for all flavors and creates flavor specific 251 | files `%{python_prefix}-foo.lang`. Additional arguments of `%find_lang` are supported. The filelist 252 | can then be used as `%files %{python_files} -f %{python_prefix}-foo.lang` in the `%files` section header. 253 | 254 | 255 | ##### Unit testing 256 | 257 | * __`%pytest`__ runs `pytest` in all flavors with appropriate environmental variables 258 | (namely, it sets `$PYTHONPATH` to ``%{$python_sitelib}``). All paramteres to this macro are 259 | passed without change to the pytest command. Explicit `BuildRequires` on `%{python_module pytest}` 260 | is still required. 261 | 262 | * __`%pytest_arch`__ the same as the above, except it sets ``$PYTHONPATH`` to ``%{$python_sitearch}``. 263 | 264 | * __`%pyunittest`__ and __`%pyunittest_arch`__ run `$python -m unittest` on all flavors with 265 | appropriate environmental variables very similar to `%pytest` and `%pytest_arch`. 266 | 267 | 268 | #### Alternative-related, general: 269 | 270 | * __`%prepare_alternative [-t ] `__ replaces `` with a symlink to 271 | `/etc/alternatives/`, plus related housekeeping. If no `` is given, it is 272 | `%{_bindir}/`. 273 | 274 | * __`%install_alternative [-n ] [-s ] [-t ] [-p ]`__ runs the 275 | `update-alternative` command, configuring `` alternative to be ``, with a 276 | priority ``. If no `` is given, it is `%{_bindir}/`. Can be followed by 277 | additional arguments to `update-alternatives`, such as `--slave`. 278 | 279 | * __`%uninstall_alternative [-n ] [-t ]`__ if uninstalling (not upgrading) the 280 | package, remove `` from a list of alternatives under `` 281 | 282 | * __`%alternative_to `__ generates a filelist entry for `` and a ghost entry for 283 | `basename ` in `/etc/alternatives` 284 | 285 | 286 | #### Alternative-related, for Python: 287 | 288 | * __`%python_alternative `__: expands to filelist entries for ``, its symlink in 289 | `/etc/alternatives`, and the target file called `-%python_bin_suffix`. 290 | In case the file is a manpage (`file.1.gz`), the target is called `file-%suffix.1.gz`. 291 | 292 | * __`%python_install_alternative [ ...]`__: runs `update-alternatives` 293 | for `-%{python_bin_suffix}` (unless `--with libalternatives` is enabled). 294 | If more than one argument is present, the remaining ones are converted to `--slave` arguments. 295 | If a `name` is in the form of `something.1` or `something.4.gz` (any number applies), it is 296 | handled as a manpage and assumed to live in the appropriate `%{_mandir}` subdirectory, otherwise 297 | it is handled as a binary and assumed to live in `%{_bindir}`. You can also supply a full path 298 | to override this behavior. 299 | 300 | * __`%python_uninstall_alternative `__: reverse of the preceding. 301 | Note that if you created a group by specifying multiple arguments to `install_alternative`, only 302 | the first one applies for `uninstall_alternative`. 303 | 304 | Each of these has a flavor-specific spelling: `%python2_alternative` etc. 305 | 306 | 307 | #### Libalternatives-related: 308 | 309 | [Libalternatives](https://github.com/openSUSE/libalternatives) provides another way for settings alternative. 310 | Instead of symlinks, the preferred executable is executed directly. Which executable is executed 311 | depends on the available alternatives installed on the system and the system and/or user configuration files. 312 | These configuration files will also be generated by the macros described above **AND** following settings in the 313 | spec file: 314 | 315 | * Enable *libalternative* by making __`--with libalternatives`__ the default: 316 | ```spec 317 | %if 0%{?suse_version} > 1500 318 | %bcond_without libalternatives 319 | %else 320 | %bcond_with libalternatives 321 | %endif 322 | ``` 323 | This example shows that *libalternatives* is available for TW only. 324 | 325 | * Require the `alts` package during build and runtime: 326 | ```spec 327 | %if %{with libalternatives} 328 | Requires: alts 329 | BuildRequires: alts 330 | %else 331 | Requires(post): update-alternatives 332 | Requires(postun):update-alternatives 333 | %endif 334 | ``` 335 | 336 | * Group entries using __`%python_group_libalternatives`__ 337 | (similar to what would have been installed as master and slaves in %python_install_alternatives, 338 | but without the manuals, as these do not go into group= entries) 339 | ```spec 340 | %install 341 | ... 342 | %python_clone -a %{buildroot}/%{_bindir}/cmd1 343 | %python_clone -a %{buildroot}/%{_binddir}/cmd2 344 | %python_clone -a %{buildroot}/%{_mandir}/man1/cmd1.1 345 | %python_group_libalternatives cmd1 cmd2 346 | ``` 347 | 348 | * Cleanup old update-alternatives entries during a transition update to libalternatives: 349 | ```spec 350 | %pre 351 | # removing old update-alternatives entries 352 | %python_libalternatives_reset_alternative 353 | ``` 354 | The argument *\* is the same used for calling *%python_uninstall_alternative*. 355 | 356 | #### Building and testing with flavored alternatives 357 | 358 | * __`%python_flavored_alternatives`__: If a build tool or a test 359 | suite calls commands, which exist in several alternatives, and 360 | you need them to call the command in the alternative of the 361 | current flavor within an `%python_expand` block, this macro 362 | 363 | - creates the appropriate update-alternatives symlinks in the 364 | shuffled `build/flavorbin` directory and sets `$PATH` 365 | accordingly, and 366 | - selects the libalternatives priority of all installed commands 367 | with a `libalternatives.conf` in 368 | `XDG_CONFIG_HOME=$PWD/build/xdgflavorconfig`. 369 | 370 | The `%pytest(_arch)` and `%pyunittest(_arch)` macros include a call 371 | of this macro before expanding to the test suite execution. 372 | 373 | #### Flavor-specific macros 374 | 375 | In addition, the following flavor-specific macros are known and supported by the configuration: 376 | 377 | * __`%__`__: path to the ```` executable. 378 | 379 | * __`%_pyproject_wheel`__ expands to PEP517 instructions to build a wheel using the particular flavor. 380 | 381 | * __`%_pyproject_install`__ expands to PEP517 install instructions for the particular flavor. 382 | 383 | * __`%_build`__ expands to `setup.py` build instructions for the particular flavor. 384 | 385 | * __`%_install`__ expands to legacy `setup.py` install instructions for the particular flavor. 386 | 387 | * __`%_fix_shebang`__ rewrites the script interpreter line in python scripts 388 | installed into `%_bindir` to use the particular flavor. In multi-flavor expansions 389 | the call of this macro is not required, as the script interpreter line is already 390 | taken care of by the alternatives setup `%python_clone -a`. 391 | 392 | * __`%_fix_shebang_path path`__ rewrites the script interpreter 393 | line in python scripts installed into `path` to use the 394 | particular flavor. A glob like `*` can be used in the path parameter 395 | to fix the shebang for all files in a directory. 396 | 397 | * __`%_sitelib`, `%_sitearch`__: path to noarch (purelib) and 398 | arch-dependent `site-packages` (platlib) directory. 399 | 400 | * __`%_version`__: dotted major.minor version. `2.7` for CPython 2.7. 401 | 402 | * __`%_version_nodots`__: concatenated major.minor version. `27` for CPython 2.7. 403 | 404 | * __`%_bin_suffix`__: what to put after 405 | a binary name. Binaries for CPython are called `binary-%{python_version}`, for PyPy the name 406 | is `binary-pp%{pypy3_version}` 407 | 408 | * __`%_prefix`__: prefix of the package name. `python` for old-style distros, `python2` for 409 | new-style. For other flavors, the value is the same as flavor name. 410 | 411 | For reasons of preferred-flavor-agnosticity, aliases `python_*` are available for all of these. 412 | 413 | We recognize `%py_ver`, `%py2_ver` and `%py3_ver` as deprecated spellings of `%_version`. No 414 | such shortcut is in place for `pypy3`. Furthermore, `%py2_build`, `_install` and `_shbang_opts`, as 415 | well as `py3` variants, are recognized for Fedora compatibility. 416 | 417 | ### `%files` section 418 | 419 | * __`%files %{python_files}`__ expands the `%files` section for all generated flavor packages of 420 | `-modname`. 421 | 422 | * __`%files %{python_files foo}`__ expands the `%files` section for all generated flavor subpackages 423 | of `-modname-foo`. 424 | 425 | For subpackages of non-python packages with `%python_subpackage_only`and 426 | `%package -n %{python_flavor}-modname`, also use `%files %{python_files modname}`. 427 | 428 | Always use the flavor-agnostic macro versions `%python_*` inside `%python_files` marked `%files` sections. 429 | 430 | See also the Filelists section of the 431 | [openSUSE:Packaging Python](https://en.opensuse.org/openSUSE:Packaging_Python#Filelists) 432 | guidelines 433 | 434 | ### Files in Repository 435 | 436 | * __`macros` directory__: contains a list of files that are concatenated to produce the resulting 437 | `macros.python_all` file. This directory is incomplete, files `020-flavor-$flavor` and 438 | `040-automagic` are generated by running the compile script. 439 | 440 | * __`macros/001-alternatives`__: macro definitions for alternatives handling. These are not 441 | Python-specific and might find their way into `update-alternatives`. 442 | 443 | * __`macros/010-common-defs`__: setup for macro spelling templates, common handling, preferred flavor 444 | configuration etc. 445 | 446 | * __`macros/030-fallbacks`__: compatibility and deprecated spellings for some macros. 447 | 448 | * __`apply-macros.sh`__: compile macros and run `rpmspec` against first argument. Useful for examining 449 | what is going on with your spec file. 450 | 451 | * __`buildset.in`__: template to generate `macros/040-buildset` for the `%pythons`, `%skip_` and 452 | `%python_module` macros. 453 | 454 | * __`compile-macros.sh`__: the compile script. Builds flavor-specific macros, Lua script definition, 455 | and concatenates all of it into `macros.python_all`. 456 | 457 | * __`flavor.in`__: template for flavor-specific macros. Generates `macros/020-flavor-` for 458 | every flavor listed in `compile-macros.sh`. 459 | 460 | * __`functions.lua`__: Lua function definitions used in `macros.lua` and elsewhere. In the compile 461 | step, these are converted to a RPM macro `%_python_definitions`, which is evaluated as part of 462 | `%_python_macro_init`. This can then be called anywhere that we need a ready-made Lua environment. 463 | 464 | * __`macros.in`__: pure-RPM-macro definitions for the single-spec generator. References and uses the 465 | private Lua macros. The line `### LUA-MACROS ###` is replaced with inlined Lua macro definitions. 466 | 467 | * __`macros.lua`__: Lua macro definitions for the single-spec generator. 468 | This is actually pseudo-Lua: the top-level functions are not functions, and are instead converted to 469 | Lua macro snippets. (That means that you can't call the top-level functions from Lua. For defining 470 | pure Lua functions that won't be available as Lua macros, use the `functions.lua` file.) 471 | 472 | * __`macros-default-pythons`__: macro definitions for `%have_python2` and `%have_python3` for systems 473 | where this is not provided by your Python installation. The spec file uses this for SUSE <= Leap 42.3. 474 | `apply-macros` also uses it implicitly (for now). 475 | 476 | * __`README.md`__: This file. As if you didn't know. 477 | -------------------------------------------------------------------------------- /apply-macros.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | REALPATH=$(realpath "${BASH_SOURCE[0]}") 4 | MYPATH=$(dirname "$REALPATH") 5 | 6 | ( 7 | cd "$MYPATH" 8 | ./compile-macros.sh 9 | ) 10 | 11 | rpmspec -v \ 12 | --macros=/usr/lib/rpm/macros:/etc/rpm/macros.python2:/etc/rpm/macros.python3:$MYPATH/macros.python_all \ 13 | -P "$1" 14 | -------------------------------------------------------------------------------- /compile-macros.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The set of flavors for which we produce macros. Not identical to 4 | # the buildset predefined for specific distributions (see below) 5 | FLAVORS="python2 python3 python39 python310 python311 python312 python313 python314 pypy3" 6 | 7 | ### flavor-specific: generate from flavor.in 8 | for flavor in $FLAVORS; do 9 | sed 's/#FLAVOR#/'$flavor'/g' flavor.in > macros/020-flavor-$flavor 10 | if [ "$flavor" = "python2" ]; then 11 | # special old-style package provides and obsoletes for python2 12 | echo "%${flavor}_provides python" >> macros/020-flavor-$flavor 13 | fi 14 | done 15 | 16 | 17 | ### buildset: %pythons, %python_module and %add_python, coming from 18 | # the current build target's prjconf 19 | echo "Setting buildset:" 20 | echo "## Python Buildset Begin" | tee macros/040-builset-start 21 | # First try to find the block from Factory 22 | sed -n '/## PYTHON MACROS BEGIN/,/## PYTHON MACROS END/ p' ~/.rpmmacros | tee macros/041-buildset 23 | # If that fails, find the old definitions (SUSE:SLE-15-SP?:GA, openSUSE:Leap:15.?) 24 | if [ ! -s macros/041-buildset ]; then 25 | sed -n '/%pythons/,/%add_python/ p' ~/.rpmmacros | tee macros/041-buildset 26 | fi 27 | # If we still have nothing (different distro, custom prjconf, building 28 | # python-rpm-macros outside of obs), use the default file 29 | if [ ! -s macros/041-buildset ]; then 30 | tee macros/041-buildset < default-prjconf 31 | fi 32 | echo "## Python Buildset End" | tee macros/042-builset-end 33 | 34 | ### Lua: generate automagic from macros.in and macros.lua 35 | ( 36 | # copy macros.in up to LUA-MACROS 37 | sed -n -e '1,/^### LUA-MACROS ###$/p' macros.in 38 | 39 | # include "functions.lua", without empty lines, as %_python_definitions 40 | echo "%_python_definitions %{lua:" 41 | sed -n -r \ 42 | -e 's/\\/\\\\/g' \ 43 | -e '/^.+$/p' \ 44 | functions.lua 45 | echo "}" 46 | 47 | INFUNC=0 48 | # brute line-by-line read of macros.lua 49 | IFS="" 50 | while read -r line; do 51 | if echo "$line" | grep -q '^function '; then 52 | # entering top-level Lua function 53 | INFUNC=1; 54 | echo "$line" | sed -r -e 's/^function (.*)\((.*)\)$/%\1(\2) %{lua: \\/' 55 | elif [ "$line" == "end" ]; then 56 | # leaving top-level Lua function 57 | INFUNC=0; 58 | echo '}' 59 | elif [ $INFUNC == 1 ]; then 60 | # inside function 61 | # double backslashes and add backslash to end of line 62 | echo "$line" | sed -e 's/\\/\\\\/g' -e 's/$/\\/' 63 | else 64 | # outside function, copy 65 | # (usually this is newline) 66 | echo "$line" 67 | fi 68 | done < macros.lua 69 | 70 | # copy rest of macros.in 71 | sed -n -e '/^### LUA-MACROS ###$/,$p' macros.in 72 | ) > macros/050-automagic 73 | 74 | 75 | ### final step: cat macros/*, but with files separated by additional newlines 76 | sed -e '$s/$/\n/' -s macros/* > macros.python_all 77 | -------------------------------------------------------------------------------- /default-prjconf: -------------------------------------------------------------------------------- 1 | # default-prjconf: buildset definitions for python-rpm-macros. 2 | # 3 | # This is usually overridden by the distribution's prjconf, landing in ~/.rpmmacros. 4 | # This file provides the default definition from Factory (Tumbleweed) for pure rpmbuild packaging. 5 | 6 | # Macros: 7 | ## PYTHON MACROS BEGIN 8 | # order of %pythons is important: The last flavor overrides any operation on conflicting files and definitions during expansions, 9 | # making it the "default" in many cases --> keep the primary python3 provider at the end. 10 | %pythons %{?!skip_python3:%{?!skip_python311:python311} %{?!skip_python312:python312} %{?!skip_python313:python313}} 11 | %add_python() %{expand:%%define pythons %1 %pythons} 12 | 13 | %_without_python2 1 14 | 15 | # This method for generating python_modules gets too deep to expand for rpm at about 5 python flavors. 16 | # Hence, python_module_iter is replaced by python_module_lua in macros.lua. 17 | # However, OBS cannot expand lua, but has a much higher expansion depth, so this works fine for the server side resolver. 18 | %python_module_iter(a:) %{expand:%%define python %{-a*}} ( %python-%args ) %{expand:%%{?!python_module_iter_%1:%%{python_module_iter -a%*}}%%{?python_module_iter_%1}} 19 | # pseudo-undefine for obs: reset for the next expansion within the next call of python_module 20 | %python_module_iter_STOP %global python %%%%python 21 | %python_module() %{?!python_module_lua:%{expand:%%define args %{**}} %{expand:%%{python_module_iter -a %{pythons} STOP}}}%{?python_module_lua:%python_module_lua %{**}} 22 | # gh#openSUSE/python-rpm-macros#127 ... define our current primary Python interpreter 23 | %primary_python python313 24 | ## PYTHON MACROS END 25 | # :Macros 26 | -------------------------------------------------------------------------------- /flavor.in: -------------------------------------------------------------------------------- 1 | ##### macro definitions for flavor "#FLAVOR#" ##### 2 | 3 | %#FLAVOR#_shbang_opts %py_shbang_opts 4 | 5 | %__#FLAVOR# %{_bindir}/%{lua: print((string.gsub("#FLAVOR#", "(%a+%d)(%d+)", "%1.%2")))} 6 | 7 | %#FLAVOR#_prefix #FLAVOR# 8 | %#FLAVOR#_sitelib %{_python_sysconfig_path %__#FLAVOR# purelib} 9 | %#FLAVOR#_sitearch %{_python_sysconfig_path %__#FLAVOR# platlib} 10 | %#FLAVOR#_version %{_python_sysconfig_var %__#FLAVOR# py_version_short} 11 | %#FLAVOR#_version_nodots %{_python_sysconfig_var %__#FLAVOR# py_version_nodot} 12 | 13 | %#FLAVOR#_sysconfig_path() %{_python_sysconfig_path %__#FLAVOR# %1} 14 | %#FLAVOR#_sysconfig_var() %{_python_sysconfig_var %__#FLAVOR# %1} 15 | 16 | %#FLAVOR#_bin_suffix %{?!_#FLAVOR#_bin_suffix:%#FLAVOR#_version}%{?_#FLAVOR#_bin_suffix} 17 | 18 | # Check if there is a major version symlink to our flavor in the current build system. If so, we are the primary provider. 19 | %#FLAVOR#_provides %(provides=""; \ 20 | for flavorbin in %{_bindir}/python?; do \ 21 | if [ $flavorbin != %__#FLAVOR# -a $(realpath $flavorbin) = %__#FLAVOR# ]; then \ 22 | provides="$provides $(basename $flavorbin)"; \ 23 | fi; \ 24 | done; \ 25 | echo ${provides# }; \ 26 | ) 27 | 28 | %if#FLAVOR# %if "%{python_flavor}" == "#FLAVOR#" 29 | 30 | %#FLAVOR#_only() %if "%{python_flavor}" == "#FLAVOR#" \ 31 | %** \ 32 | %endif 33 | 34 | %#FLAVOR#_build \ 35 | %{_python_use_flavor #FLAVOR#} \ 36 | %__#FLAVOR# %{py_setup} %{?py_setup_args} build \\\ 37 | --executable="%__#FLAVOR# %#FLAVOR#_shbang_opts" 38 | 39 | %#FLAVOR#_install(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) \ 40 | %{_python_use_flavor #FLAVOR#} \ 41 | myargs="%{**}" \ 42 | %__#FLAVOR# %{py_setup} %{?py_setup_args} install \\\ 43 | -O1 --skip-build --force --root %{buildroot} --prefix %{_prefix} $myargs \ 44 | %#FLAVOR#_compile \ 45 | %#FLAVOR#_fix_shebang 46 | 47 | %#FLAVOR#_pytest(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) \ 48 | %{_python_use_flavor #FLAVOR#} \ 49 | myargs="%{**}" \ 50 | PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}%{buildroot}%{#FLAVOR#_sitelib} PYTHONDONTWRITEBYTECODE=1 %__#FLAVOR# -m pytest -v $myargs 51 | 52 | %#FLAVOR#_pytest_arch(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) \ 53 | %{_python_use_flavor #FLAVOR#} \ 54 | myargs="%{**}" \ 55 | PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}%{buildroot}%{#FLAVOR#_sitearch} PYTHONDONTWRITEBYTECODE=1 %__#FLAVOR# -m pytest -v $myargs 56 | 57 | %#FLAVOR#_pyunittest(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) \ 58 | %{_python_use_flavor #FLAVOR#} \ 59 | myargs="%{**}" \ 60 | PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}%{buildroot}%{#FLAVOR#_sitelib} PYTHONDONTWRITEBYTECODE=1 %__#FLAVOR# -m unittest $myargs 61 | 62 | %#FLAVOR#_pyunittest_arch(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) \ 63 | %{_python_use_flavor #FLAVOR#} \ 64 | myargs="%{**}" \ 65 | PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}%{buildroot}%{#FLAVOR#_sitearch} PYTHONDONTWRITEBYTECODE=1 %__#FLAVOR# -m unittest $myargs 66 | 67 | ##### PEP517/PEP518 macros ##### 68 | 69 | %#FLAVOR#_pyproject_wheel(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) \ 70 | %{_python_use_flavor #FLAVOR#} \ 71 | myargs="%{**}" \ 72 | if [ -f %{_pyproject_anywheeldir}/*-none-any.whl ]; then \ 73 | echo "Already found a compatible wheel in %{_pyproject_anywheeldir}" \ 74 | mkdir -p %{_pyproject_wheeldir} \ 75 | cp %{_pyproject_anywheeldir}/*-none-any.whl %{_pyproject_wheeldir}/ \ 76 | else \ 77 | %__#FLAVOR# -mpip wheel %{pyproject_wheel_args} ${myargs:-.}\ 78 | if [ -f %{_pyproject_wheeldir}/*-none-any.whl ]; then \ 79 | mkdir -p %{_pyproject_anywheeldir} \ 80 | cp %{_pyproject_wheeldir}/*-none-any.whl %{_pyproject_anywheeldir}/ \ 81 | fi \ 82 | fi 83 | 84 | %#FLAVOR#_pyproject_install(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) \ 85 | %{_python_use_flavor #FLAVOR#} \ 86 | myargs="%{**}" \ 87 | havereq=0 \ 88 | if [ -n "${myargs}" ]; then \ 89 | for a in ${myargs}; do \ 90 | pep440req=$(echo $(basename -- ${a}) | sed -E 's/([^-]+)-([^-]+)-.+\\.whl/\\1==\\2/') \ 91 | if [ -f ${a} -a $(basename -- ${a}) != "${pep440req}" ]; then \ 92 | myargs=$(echo ${myargs} | sed "s|${a}|${pep440req}|") \ 93 | mkdir -p %{_pyproject_wheeldir} \ 94 | cp $a %{_pyproject_wheeldir} \ 95 | havereq=1 \ 96 | fi \ 97 | done \ 98 | fi \ 99 | if [ $havereq -eq 0 ]; then \ 100 | # Fallback for old macro usages which expect a wheel in the current directory \ 101 | ls %{_pyproject_wheeldir}/*.whl || (mkdir -p %{_pyproject_wheeldir}/; cp *.whl %{_pyproject_wheeldir}/) \ 102 | for w in %{_pyproject_wheeldir}/*.whl; do \ 103 | myargs="$myargs $(echo $(basename ${w}) | sed -E 's/([^-]+)-([^-]+)-.+\\.whl/\\1==\\2/')" \ 104 | done \ 105 | fi \ 106 | %__#FLAVOR# -mpip install %{pyproject_install_args} $myargs \ 107 | %#FLAVOR#_compile \ 108 | %#FLAVOR#_fix_shebang 109 | 110 | %#FLAVOR#_compile \ 111 | for d in %{buildroot}%{#FLAVOR#_sitelib} %{buildroot}%{#FLAVOR#_sitearch}; do \ 112 | if [ -d $d ]; then \ 113 | find $d -iname '*.pyc' -delete \ 114 | find $d -iname '*.py' -print0 | xargs -0 %__#FLAVOR# -c ' \ 115 | import sys, py_compile \ 116 | for f in sys.argv[1:]: \ 117 | fp=f[len("%{buildroot}"):] \ 118 | print("Generating cached byte-code for " + str(fp)) \ 119 | if sys.version[0] == "2": \ 120 | py_compile.compile(f, dfile=fp) \ 121 | else: \ 122 | for o in [0, 1]: \ 123 | py_compile.compile(f, dfile=fp, optimize=o) \ 124 | ' \ 125 | fi \ 126 | done 127 | 128 | %#FLAVOR#_fix_shebang \ 129 | %#FLAVOR#_fix_shebang_path %{buildroot}%{_bindir}/* %{buildroot}%{_sbindir}/* 130 | 131 | %#FLAVOR#_fix_shebang_path(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) \ 132 | myargs="%{**}" \ 133 | for f in ${myargs}; do \ 134 | if [ -f "$f" -a -x "$f" -a -w "$f" ] \ 135 | then \ 136 | # in i586, sed fails when following symlinks to long paths, so \ 137 | # changing to the target directory avoid this problem \ 138 | cd "$(dirname "$f")" \ 139 | sed -i --follow-symlinks "1s@#\\!.*python\\S*@#\\!$(realpath %__#FLAVOR#)@" "$(basename "$f")" \ 140 | cd - \ 141 | fi \ 142 | done 143 | 144 | # Alternative entries in file section 145 | 146 | %#FLAVOR#_alternative() %{_python_macro_init} \ 147 | %{lua:local link, name, path = python_alternative_names(rpm.expand("%1"), rpm.expand("%#FLAVOR#_bin_suffix")) \ 148 | local libalternatives = rpm.expand("%{with libalternatives}") \ 149 | if libalternatives == "1" then \ 150 | if not link:startswith(rpm.expand("%{_mandir}")) then \ 151 | local prio = alternative_prio("#FLAVOR#") \ 152 | print(rpm.expand("%dir %{_datadir}/libalternatives/" .. name .. "\\\n")) \ 153 | print(rpm.expand("%{_datadir}/libalternatives/" .. name .. "/" .. prio .. ".conf\\\n")) \ 154 | print(link .. "\\\n") \ 155 | end \ 156 | print(path .. "\\\n") \ 157 | else \ 158 | print(rpm.expand("%ghost %{_sysconfdir}/alternatives/" .. name .. "\\\n")) \ 159 | print(link .. "\\\n") \ 160 | print(path .. "\\\n") \ 161 | end \ 162 | } 163 | 164 | # Commands for RPM scriptlets: These must not be empty even if there is no operation for 165 | # either update-alternatives or libalternatives. 166 | 167 | %#FLAVOR#_install_alternative() \# #FLAVOR#_install_alternative: \ 168 | %{?!with_libalternatives:%{_python_macro_init}%{lua:python_install_ualternative("#FLAVOR#") \ 169 | }}%{?with_libalternatives:\: \# no install scriptlet action for libalternatives} 170 | 171 | %#FLAVOR#_uninstall_alternative() \# #FLAVOR#_uninstall_alternative: \ 172 | %{?!with_libalternatives:%{uninstall_alternative -n %1 -t %{_bindir}/%1-%{#FLAVOR#_bin_suffix} \ 173 | }}%{?with_libalternatives:\: \# no uninstall scriptlet action for libalternatives} 174 | 175 | %#FLAVOR#_reset_alternative() \# #FLAVOR#_reset_alternative: \ 176 | %{?!with_libalternatives:\: \# reset action only for libalternatives \ 177 | }%{?with_libalternatives:%{reset_alternative -n %1 -t %{_bindir}/%1-%{#FLAVOR#_bin_suffix}}} 178 | -------------------------------------------------------------------------------- /functions.lua: -------------------------------------------------------------------------------- 1 | -- declare common functions 2 | function string.startswith(str, prefix) 3 | return str:sub(1, prefix:len()) == prefix 4 | end 5 | 6 | function string.endswith(str, suffix) 7 | return str:sub(-suffix:len()) == suffix 8 | end 9 | 10 | function string.basename(str) 11 | while true do 12 | local idx = str:find("/") 13 | if not idx then return str end 14 | str = str:sub(idx + 1) 15 | end 16 | end 17 | 18 | function lookup_table(tbl) 19 | local result = {} 20 | for _,v in ipairs(tbl) do result[v] = true end 21 | return result 22 | end 23 | 24 | -- macro replacements 25 | SHORT_FLAVORS = { 26 | -- ?? 27 | python = "py", 28 | -- ?? 29 | python2 = "py2", 30 | python3 = "py3", 31 | pypy = "pypy", 32 | } 33 | 34 | function replace_macros(str, targetflavor) 35 | local LONG_MACROS = { "sitelib", "sitearch", 36 | "alternative", "install_alternative", "uninstall_alternative", "reset_alternative", 37 | "version", "version_nodots", "bin_suffix", "prefix", "provides"} 38 | local SHORT_MACROS = { "ver" } 39 | for _, srcflavor in ipairs({flavor, "python"}) do 40 | str = str:gsub("%%__" .. srcflavor, "%%__" .. targetflavor) 41 | for _, macro in ipairs(LONG_MACROS) do 42 | local from = string.format("%s_%s", srcflavor, macro) 43 | local to = string.format("%s_%s", targetflavor, macro) 44 | str = str:gsub("%%" .. from, "%%" .. to) 45 | str = str:gsub("%%{" .. from .. "}", "%%{" .. to .. "}") 46 | str = str:gsub("%%{" .. from .. "(%s+.-)}", "%%{" .. to .. "%1}") 47 | end 48 | for _, macro in ipairs(SHORT_MACROS) do 49 | local from = string.format("%s_%s", SHORT_FLAVORS[srcflavor], macro) 50 | local to = string.format("%s_%s", SHORT_FLAVORS[targetflavor], macro) 51 | str = str:gsub("%%" .. from, "%%" .. to) 52 | str = str:gsub("%%{" .. from .. "}", "%%{" .. to .. "}") 53 | end 54 | end 55 | return str 56 | end 57 | 58 | function package_name(flavor, modname, subpkg, append) 59 | if flavor == "python2" and old_python2 then 60 | flavor = "python" 61 | end 62 | local name = flavor .. "-" .. modname 63 | if subpkg and subpkg ~= "" then 64 | name = name .. "-" .. subpkg 65 | end 66 | if append and append ~= "" then 67 | name = name .. " " .. replace_macros(append, flavor) 68 | end 69 | return name 70 | end 71 | 72 | -- alternative-related 73 | local bindir = rpm.expand("%{_bindir}") 74 | local mandir = rpm.expand("%{_mandir}") 75 | local ext_man, ext_man_expr 76 | ext_man = rpm.expand("%{ext_man}") 77 | if ext_man == "" then 78 | ext_man_expr = "%.%d$" 79 | else 80 | -- ASSUMPTION: ext_man:startswith(".") 81 | ext_man_expr = "%.%d%" .. ext_man .. "$" 82 | end 83 | 84 | function python_alternative_names(arg, binsuffix, keep_path_unmangled) 85 | local link, name, path 86 | name = arg:basename() 87 | local man_ending = arg:match(ext_man_expr) or arg:match("%.%d$") 88 | if arg:startswith("/") then 89 | link = arg 90 | elseif man_ending then 91 | link = mandir .. "/man" .. man_ending:sub(2,2) .. "/" .. arg 92 | else 93 | link = bindir .. "/" .. arg 94 | end 95 | if man_ending then 96 | path = link:sub(1, -man_ending:len()-1) .. "-" .. binsuffix .. man_ending 97 | else 98 | path = link .. "-" .. binsuffix 99 | end 100 | 101 | -- now is the time to append ext_man if appropriate 102 | -- "link" and "name" get ext_man always 103 | if ext_man ~= "" and man_ending and not arg:endswith(ext_man) then 104 | link = link .. ext_man 105 | name = name .. ext_man 106 | if not keep_path_unmangled then path = path .. ext_man end 107 | end 108 | return link, name, path 109 | end 110 | function alternative_prio(flavor) 111 | local prio = rpm.expand("%" .. flavor .. "_version_nodots") 112 | -- increase priority for primary python3 flavor 113 | local provides = rpm.expand("%" .. flavor .. "_provides") .. " " 114 | if provides:match("python3%s") then 115 | prio = prio + 1000 116 | end 117 | return prio 118 | end 119 | function python_install_ualternative(flavor) 120 | local prio = alternative_prio(flavor) 121 | local binsuffix = rpm.expand("%" .. flavor .. "_bin_suffix") 122 | 123 | local params = {} 124 | for p in string.gmatch(rpm.expand("%*"), "%S+") do 125 | table.insert(params, p) 126 | end 127 | 128 | if #params == 0 then 129 | print("error") 130 | return 131 | end 132 | 133 | local link, name, path = python_alternative_names(params[1], binsuffix) 134 | print(string.format("update-alternatives --quiet --install %s %s %s %s", link, name, path, prio)) 135 | table.remove(params, 1) 136 | for _, v in ipairs(params) do 137 | print(string.format(" \\\n --slave %s %s %s", python_alternative_names(v, binsuffix))) 138 | end 139 | end 140 | function python_install_libalternative(flavor, target) 141 | local prio = alternative_prio(flavor) 142 | local binsuffix = rpm.expand("%" .. flavor .. "_bin_suffix") 143 | local ldir = rpm.expand("%{buildroot}%{_datadir}/libalternatives") 144 | local link, name, path = python_alternative_names(target, binsuffix) 145 | local man_ending = name:match(ext_man_expr) 146 | local entry, lname 147 | if man_ending then 148 | lname=name:sub(1,-ext_man:len()-3) 149 | entry="man=" .. path:basename():sub(1,-ext_man:len()-1) 150 | else 151 | entry="binary=" .. path 152 | lname=name 153 | end 154 | print(string.format("mkdir -p %s/%s\n", ldir, lname)) 155 | print(string.format("echo %s >> %s/%s/%s.conf\n", entry, ldir, lname, prio)) 156 | end 157 | -------------------------------------------------------------------------------- /macros-default-pythons: -------------------------------------------------------------------------------- 1 | # this file should be excluded for new distributions, 2 | # where %have_$flavor is determined by installed python packages 3 | # (see python-rpm-macros.spec) 4 | 5 | %have_python2 1 6 | %have_python3 1 7 | -------------------------------------------------------------------------------- /macros.in: -------------------------------------------------------------------------------- 1 | %python_flavor %{_python_macro_init}%{lua: print(flavor)} 2 | 3 | %if_python_kind() %if "%{python_flavor}" == "%1" 4 | %if_not_python_kind() %if "%{python_flavor}" != "%1" 5 | 6 | %ifpycache %if "%{python_flavor}" != "python2" 7 | 8 | %pycache_only() %ifpycache \ 9 | %** \ 10 | %endif 11 | 12 | %_python_use_flavor() \ 13 | last_flavor=`[ -f _current_flavor ] && cat _current_flavor || true` \ 14 | if [ -z "$last_flavor" ]; then last_flavor="tmp"; fi \ 15 | if [ "$last_flavor" != "%1" ]; then \ 16 | if [ -d build ]; then mv build _build.$last_flavor; fi \ 17 | if [ -d _build.%1 ]; then mv _build.%1 build; fi \ 18 | fi \ 19 | echo %1 > _current_flavor \ 20 | python_flavor=%1 \ 21 | %{nil} 22 | 23 | %_python_stash_flavor() \ 24 | if [ -d build ]; then mv build _build.%1; fi \ 25 | if [ -d _build.tmp ]; then mv _build.tmp build; fi \ 26 | %{nil} 27 | 28 | 29 | ### LUA-MACROS ### 30 | 31 | 32 | %_python_macro_init %{_python_definitions}%{_python_scan_spec}%{lua: rpm.define("_python_macro_init %{nil}")} 33 | -------------------------------------------------------------------------------- /macros.lua: -------------------------------------------------------------------------------- 1 | function _python_scan_spec() 2 | local last_python = rpm.expand("%python_for_executables") 3 | local insert_last_python = false 4 | 5 | pythons = {} 6 | -- make sure that last_python is the last item in the list 7 | for str in string.gmatch(rpm.expand("%pythons"), "%S+") do 8 | if str == last_python then 9 | insert_last_python = true 10 | else 11 | table.insert(pythons, str) 12 | end 13 | end 14 | -- ...but check that it is actually in the buildset 15 | if insert_last_python then table.insert(pythons, last_python) end 16 | 17 | modname = rpm.expand("%name") 18 | local spec_name_prefix = "python" 19 | -- modname from name 20 | local name = modname 21 | for _,py in ipairs(pythons) do 22 | if name:find(py .. "%-") == 1 then 23 | spec_name_prefix = py 24 | modname = name:sub(py:len() + 2) 25 | break 26 | end 27 | end 28 | -- try to match "python-" 29 | if name == modname and name:find("python%-") == 1 then 30 | spec_name_prefix = "python" 31 | modname = name:sub(8) 32 | end 33 | -- if not found, modname == %name, spec_name_prefix == "python" 34 | 35 | system_python = rpm.expand("%system_python") 36 | -- is the package built for python2 as "python-foo" ? 37 | old_python2 = rpm.expand("%python2_prefix") == "python" 38 | is_called_python = spec_name_prefix == "python" 39 | 40 | -- detect `flavor`, used for evaluating %ifmacros 41 | if is_called_python then 42 | -- either system_python (if found in %pythons) 43 | -- or the last entry of %pythons 44 | for _,py in ipairs(pythons) do 45 | flavor = py 46 | if flavor == system_python then break end 47 | end 48 | else 49 | -- specname is something other than "python-", and it is a valid 50 | -- python flavor (otherwise spec_name_prefix defaults to "python" 51 | -- so `is_called_python` is true), so we use it literally 52 | flavor = spec_name_prefix 53 | end 54 | 55 | -- find the spec file 56 | specpath = rpm.expand("%_specfile") 57 | end 58 | 59 | function python_subpackages() 60 | rpm.expand("%_python_macro_init") 61 | _python_subpackages_emitted = true 62 | 63 | local current_flavor = flavor 64 | local original_flavor = rpm.expand("%python_flavor") 65 | 66 | subpackage_only = rpm.expand("%{python_subpackage_only}") == "1" 67 | if subpackage_only then 68 | is_called_python = false 69 | modname = "" 70 | end 71 | 72 | -- line processing functions 73 | local function print_altered(line) 74 | -- set %name macro to proper flavor-name 75 | if not subpackage_only then 76 | line = line:gsub("%%{?name}?", current_flavor .. "-" .. modname) 77 | end 78 | -- print expanded 79 | print(rpm.expand(replace_macros(line, current_flavor)) .. "\n") 80 | end 81 | 82 | local function ignore_line(line) end 83 | 84 | local function files_line(line) 85 | -- unexpand %license at start of line 86 | if line:startswith("%license") then 87 | line = "%" .. line 88 | end 89 | return print_altered(line) 90 | end 91 | 92 | local PROPERTY_COPY_UNMODIFIED = lookup_table { "Summary:", "Version:", "BuildArch:" } 93 | local PROPERTY_COPY_MODIFIED = lookup_table { 94 | "Requires:", "Provides:", 95 | "Recommends:", "Suggests:", 96 | "Conflicts:", "Obsoletes:", 97 | "Supplements:", "Enhances:", 98 | "%requires_eq", "%requires_ge", 99 | "Requires(pre):", "Requires(preun):", "Requires(post):", "Requires(postun):", 100 | "Requires(pretrans):", "Requires(posttrans):", 101 | } 102 | local PROPERTY_COPY_DEFAULT_PROVIDER = lookup_table { 103 | "Conflicts:", "Obsoletes:", "Provides:", "Supplements:", "Enhances:", 104 | } 105 | 106 | local function process_package_line(line) 107 | -- This function processes package tags like requirements and capabilities. 108 | -- It supports the python- prefix for plain packages, packageand(python-a:python-b:...), and boolean dependencies. 109 | -- "Requires: python-foo" -> "Requires: python3-foo" 110 | -- "Requires: %{name} = %{version}" -> "Requires: python3-modname = %{version}" 111 | 112 | -- first split Property: value 113 | local property, value = line:match("^([A-Z%%]%S+)%s*(.*)$") 114 | 115 | -- split and rewrite every package value either plain or inside boolean dependencies and packageand() -- recursive 116 | local function replace_prefix(value, flavor) 117 | local function replace_prefix_r(ivalue) 118 | return replace_prefix(ivalue, flavor) 119 | end 120 | local function rename_package(package) 121 | if package == "python" or package == flavor then 122 | -- specialcase plain "python" 123 | package = current_flavor 124 | else 125 | package = package:gsub("^" .. flavor .. "(%W)", current_flavor .. "%1") 126 | package = package:gsub("^python(%W)", current_flavor .. "%1") 127 | end 128 | return package 129 | end 130 | local before, inner, space, remainder 131 | inner, space, remainder = value:match("^packageand(%b())(%s*)(.*)$") 132 | if inner then 133 | return "packageand(" .. inner:sub(2,-2):gsub("[^:]+", rename_package) .. ")" .. space .. replace_prefix_r(tostring(remainder)) 134 | end 135 | before, inner, space, remainder = value:match("^([^()]*)(%b())(%s*)(.*)$") 136 | if inner then 137 | return replace_prefix_r(tostring(before)) .. "(".. replace_prefix_r(inner:sub(2, -2)) .. ")" .. space .. replace_prefix_r(tostring(remainder)) 138 | end 139 | return value:gsub("%S+", rename_package) 140 | end 141 | 142 | if PROPERTY_COPY_UNMODIFIED[property] then 143 | print_altered(line) 144 | elseif PROPERTY_COPY_MODIFIED[property] then 145 | -- specifically handle %name macro before expansion 146 | if not subpackage_only then 147 | line = line:gsub("%%{?name}?", current_flavor .. "-" .. modname) 148 | end 149 | local function print_property_copy_modified(value) 150 | value = replace_prefix(value, flavor) 151 | -- rely on print_altered to perform expansion on the result 152 | print_altered(string.format("%s %s", property, value)) 153 | end 154 | if PROPERTY_COPY_DEFAULT_PROVIDER[property] then 155 | -- print renamed lines for all flavors which the current_flavor provides. 156 | for iflavor in string.gmatch(rpm.expand("%{?" .. current_flavor .. "_provides}") .. " " .. current_flavor, "%S+" ) do 157 | current_flavor = iflavor -- make sure to process the main current_flavor last for final reset. 158 | print_property_copy_modified(value) 159 | end 160 | else 161 | print_property_copy_modified(value) 162 | end 163 | 164 | end 165 | end 166 | 167 | local auto_posttrans = {} 168 | local auto_posttrans_current = {} 169 | local auto_posttrans_backslash = false 170 | 171 | local function expect_alternatives(line) 172 | if auto_posttrans_backslash then 173 | local apc = auto_posttrans_current 174 | apc[#apc] = apc[#apc] .. "\n" .. line 175 | auto_posttrans_backslash = line:endswith("\\") 176 | elseif line:startswith("%python_install_alternative") 177 | or line:startswith("%{python_install_alternative") -- "}" 178 | or line:startswith("%" .. flavor .. "_install_alternative") 179 | or line:startswith("%{" .. flavor .. "_install_alternative") -- "}" 180 | then 181 | table.insert(auto_posttrans_current, line) 182 | auto_posttrans_backslash = line:endswith("\\") 183 | else 184 | auto_posttrans_backslash = false 185 | end 186 | return print_altered(line) 187 | end 188 | -- end line processing functions 189 | 190 | local function print_provided_flavor(modname) 191 | for provided_flavor in string.gmatch(rpm.expand("%{?" .. current_flavor .. "_provides}"), "%S+" ) do 192 | local pkg = provided_flavor .. "-" .. modname 193 | print(rpm.expand("Obsoletes: " .. pkg .. " < %{?epoch:%{epoch}:}%{version}-%{release}\n")) 194 | print(rpm.expand("Provides: " .. pkg .. " = %{?epoch:%{epoch}:}%{version}-%{release}\n")) 195 | end 196 | end 197 | 198 | local function section_headline(section, flavor, param) 199 | if not param then param = "" end 200 | local subpkg = " " .. param; local flags = "" 201 | for flag in subpkg:gmatch("(%s%-[flp]%s+%S+)") do 202 | flags = flags .. flag 203 | end 204 | subpkg = subpkg:gsub("(%s%-[flp]%s+%S+)", "") 205 | subpkg = subpkg:gsub("^%s*(.-)%s*$", "%1") 206 | if section == "files" then 207 | local python_files = param:match("%%{?python_files}?") 208 | local filessubpkg = param:match("%%{python_files%s*(.-)}") 209 | if filessubpkg then python_files = true end 210 | if is_called_python and not python_files then 211 | -- kingly hack. but RPM's native %error does not work. 212 | local errmsg = 213 | 'error: Package with "python-" prefix must not contain unmarked "%files" sections.\n' .. 214 | 'error: Use "%files %python_files" or "%files %{python_files foo} instead.\n' 215 | io.stderr:write(errmsg) 216 | print(errmsg) 217 | error('Invalid spec file') 218 | end 219 | if python_files then subpkg = filessubpkg end 220 | end 221 | return "%" .. section .. " -n " .. package_name(flavor, modname, subpkg, flags) .. "\n" 222 | end 223 | 224 | local python2_binsuffix = rpm.expand("%python2_bin_suffix") 225 | local function dump_alternatives_posttrans() 226 | if not old_python2 and current_flavor == "python2" then 227 | for label, value in pairs(auto_posttrans) do 228 | if value ~= false then 229 | print(section_headline("posttrans", current_flavor, label)) 230 | for _,line in ipairs(value) do 231 | -- RPM needs {} characters in Lua macros to match, so 232 | -- this is an opening "{" for this one: ----------v 233 | firstarg = line:match("install_alternative%s+([^%s}]+)") 234 | if firstarg then 235 | local _,_,path = python_alternative_names(firstarg, python2_binsuffix) 236 | print(string.format('if [ -e "%s" ]; then\n', path)) 237 | print_altered(line) 238 | print("fi\n") 239 | end 240 | end 241 | end 242 | end 243 | end 244 | auto_posttrans = {} 245 | end 246 | 247 | local function should_expect_alternatives(section, param) 248 | if old_python2 or current_flavor ~= "python2" then return false end 249 | if param == nil then param = "" end 250 | if section == "posttrans" then 251 | auto_posttrans[param] = false 252 | return false 253 | end 254 | if section == "post" and auto_posttrans[param] ~= false then 255 | auto_posttrans_current = {} 256 | auto_posttrans[param] = auto_posttrans_current 257 | return true 258 | end 259 | return false 260 | end 261 | 262 | local function match_braces(line) 263 | local count = 0 264 | for c in line:gmatch(".") do 265 | if c == "{" then count = count + 1 266 | elseif c == "}" and count > 0 then count = count - 1 267 | end 268 | end 269 | return count == 0 270 | end 271 | 272 | local KNOWN_SECTIONS = lookup_table {"package", "description", "files", "prep", 273 | "build", "install", "check", "clean", "pre", "post", "preun", "postun", 274 | "pretrans", "posttrans", "changelog"} 275 | local COPIED_SECTIONS = lookup_table {"description", "files", 276 | "pre", "post", "preun", "postun", "pretrans", "posttrans"} 277 | 278 | -- before we start, print Provides: python2-modname 279 | if is_called_python and old_python2 and not subpackage_only then 280 | print(rpm.expand("Provides: python2-" .. modname .. " = %{?epoch:%{epoch}:}%{version}-%{release}\n")) 281 | end 282 | 283 | for _,python in ipairs(pythons) do 284 | local is_current_flavor = python == flavor 285 | -- "python-foo" case: 286 | if is_called_python then 287 | if old_python2 then 288 | -- if we're in old-style package, "python" == "python2" 289 | is_current_flavor = python == "python2" 290 | else 291 | -- else nothing is current flavor, always generate 292 | is_current_flavor = false 293 | end 294 | end 295 | 296 | current_flavor = python 297 | 298 | -- rescan spec for each flavor 299 | if not is_current_flavor or subpackage_only then 300 | local spec, err = io.open(specpath, "r") 301 | if err then print ("could not find spec file at path: " .. specpath) return end 302 | 303 | rpm.define("python_flavor " .. python) 304 | 305 | local section_function 306 | 307 | if subpackage_only then 308 | section_function = ignore_line 309 | else 310 | section_function = process_package_line 311 | print(section_headline("package", current_flavor, nil)) 312 | print_provided_flavor(modname) 313 | end 314 | 315 | while true do 316 | -- collect lines until braces match. it's what rpm does, kind of. 317 | local eof = false 318 | local line = spec:read() 319 | if line == nil then break end 320 | while not match_braces(line) do 321 | local nl = spec:read() 322 | if nl == nil then eof = true break end 323 | line = line .. "\n" .. nl 324 | end 325 | if eof then break end 326 | --io.stderr:write(current_flavor .. " >".. tostring(line) .."<\n") 327 | 328 | -- match section delimiter 329 | local section_noparam = line:match("^%%(%S+)(%s*)$") 330 | local section_withparam, param = line:match("^%%(%S+)%s+(.+)$") 331 | local newsection = section_noparam or section_withparam 332 | 333 | if KNOWN_SECTIONS[newsection] then 334 | -- enter new section 335 | local ignore_section = false 336 | if subpackage_only then 337 | ignore_section = true 338 | if param then 339 | local subparam 340 | if newsection == "files" then 341 | subparam = param:match("%%{python_files%s+(.*)}") 342 | else 343 | subparam = param:match("^%-n%s+python%-(.*)$") 344 | end 345 | if subparam then 346 | local submodname, subsubparam = rpm.expand(subparam):match("^(%S+)%s*(.*)$") 347 | modname = submodname 348 | param = subsubparam 349 | ignore_section = false 350 | end 351 | end 352 | elseif (param and param:startswith("-n")) then 353 | ignore_section = true 354 | end 355 | if ignore_section then 356 | section_function = ignore_line 357 | elseif newsection == "package" then 358 | print(section_headline("package", current_flavor, param)) 359 | if subpackage_only then 360 | print_provided_flavor(modname) 361 | else 362 | -- only valid param is a regular subpackage name 363 | print_provided_flavor(modname .. "-" .. param) 364 | end 365 | section_function = process_package_line 366 | elseif newsection == "files" and current_flavor == flavor then 367 | section_function = ignore_line 368 | elseif COPIED_SECTIONS[newsection] then 369 | print(section_headline(newsection, current_flavor, param)) 370 | if should_expect_alternatives(newsection, param) then 371 | section_function = expect_alternatives 372 | elseif newsection == "files" then 373 | section_function = files_line 374 | else 375 | section_function = print_altered 376 | end 377 | else 378 | section_function = ignore_line 379 | end 380 | elseif line:startswith("%python_subpackages") then 381 | -- ignore 382 | elseif line:startswith("%if") then 383 | -- RPM handles %if on top level, whole sections can be conditional. 384 | -- We must copy the %if declarations always, even if they are part 385 | -- of non-copied sections. Otherwise we miss this: 386 | -- %files A 387 | -- /bin/something 388 | -- %if %condition 389 | -- %files B 390 | -- /bin/otherthing 391 | -- %endif 392 | print_altered(line) 393 | -- We are, however, copying expanded versions. This way, specifically, 394 | -- macros like %ifpython3 are evaluated differently in the top-level spec 395 | -- itself and in the copied sections. 396 | --io.stderr:write(rpm.expand(line) .. "\n") 397 | elseif line:startswith("%else") or line:startswith("%endif") then 398 | print(line .. "\n") 399 | --io.stderr:write(line .. "\n") 400 | else 401 | section_function(line) 402 | end 403 | end 404 | 405 | dump_alternatives_posttrans() 406 | 407 | spec:close() 408 | end 409 | end 410 | 411 | -- restore %python_flavor for further processing 412 | rpm.define("python_flavor " .. original_flavor) 413 | end 414 | 415 | function python_exec(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-=) 416 | local args = rpm.expand("%**") 417 | print(rpm.expand("%{python_expand $python " .. args .. "}")) 418 | end 419 | 420 | function python_expand(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-=) 421 | -- force spec scan 422 | rpm.expand("%_python_macro_init") 423 | local args = rpm.expand("%**") 424 | for _, python in ipairs(pythons) do 425 | print(rpm.expand("%{_python_use_flavor " .. python .. "}\n")) 426 | local cmd = replace_macros(args, python) 427 | -- when used as call of the executable, basename only 428 | cmd = cmd:gsub("$python%f[%s\"\'\\%)&|;<>]", string.basename(rpm.expand("%__" .. python))) 429 | -- when used as flavor expansion for a custom macro 430 | cmd = cmd:gsub("$python", python) 431 | print(rpm.expand(cmd .. "\n")) 432 | end 433 | end 434 | 435 | function python_build(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-=) 436 | rpm.expand("%_python_macro_init") 437 | for _, python in ipairs(pythons) do 438 | print(rpm.expand("%" .. python .. "_build %**")) 439 | end 440 | end 441 | 442 | function python_install(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-=) 443 | rpm.expand("%_python_macro_init") 444 | for _, python in ipairs(pythons) do 445 | print(rpm.expand("%" .. python .. "_install %**")) 446 | end 447 | end 448 | 449 | function pyproject_wheel(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) 450 | rpm.expand("%_python_macro_init") 451 | for _, python in ipairs(pythons) do 452 | print(rpm.expand("%" .. python .. "_pyproject_wheel %**")) 453 | end 454 | end 455 | 456 | function pyproject_install(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) 457 | rpm.expand("%_python_macro_init") 458 | for _, python in ipairs(pythons) do 459 | print(rpm.expand("%" .. python .. "_pyproject_install %**")) 460 | end 461 | end 462 | 463 | function python_compileall() 464 | rpm.expand("%_python_macro_init") 465 | for _, python in ipairs(pythons) do 466 | print(rpm.expand("%" .. python .. "_compile")) 467 | end 468 | end 469 | 470 | function python_files() 471 | rpm.expand("%_python_macro_init") 472 | local nparams = rpm.expand("%#") 473 | local param = "" 474 | if tonumber(nparams) > 0 then param = rpm.expand("%1") end 475 | 476 | if subpackage_only then 477 | modname = param 478 | param = "" 479 | end 480 | 481 | print("-n " .. package_name(flavor, modname, param)) 482 | 483 | if not _python_subpackages_emitted then 484 | print("\n/%python_subpackages_macro_not_present\n") 485 | io.stderr:write("%python_subpackages macro not present\n" 486 | .. "(To get rid of this error, either add a %python_subpackages macro to preamble " 487 | .. "or remove %python_files.\n") 488 | error("%python_subpackages macro not present\n") 489 | end 490 | end 491 | 492 | function python_clone(a) 493 | rpm.expand("%_python_macro_init") 494 | local param = rpm.expand("%1") 495 | local link, name, path 496 | for _, python in ipairs(pythons) do 497 | local binsuffix = rpm.expand("%" .. python .. "_bin_suffix") 498 | link,name,path = python_alternative_names(param, binsuffix, true) 499 | print(rpm.expand(string.format("cp %s %s\n", param, path))) 500 | print(rpm.expand(string.format("sed -ri \"1s@#!.*python\\S*@#!%s@\" %s\n", "$(realpath %__" .. python .. ")", path))) 501 | end 502 | 503 | -- %python_clone -a 504 | if rpm.expand("%{?-a}") == "-a" then 505 | local buildroot = rpm.expand("%{buildroot}") 506 | if link:startswith(buildroot) then link = link:sub(buildroot:len() + 1) end 507 | print(rpm.expand(string.format("%%{prepare_alternative -t %s %s}\n", link, name))) 508 | if rpm.expand("%{with libalternatives}") == "1" then 509 | for _, python in ipairs(pythons) do 510 | python_install_libalternative(python, link) 511 | end 512 | end 513 | end 514 | end 515 | 516 | -- called by %python_module, see buildset.in 517 | function python_module_lua() 518 | rpm.expand("%_python_macro_init") 519 | local params = rpm.expand("%**") 520 | -- The Provides: tag does not support boolean dependencies, so only add parens if needed 521 | local lpar = "" 522 | local rpar = "" 523 | local OPERATORS = lookup_table { 'and', 'or', 'if', 'with', 'without', 'unless'} 524 | for p in string.gmatch(params, "%S+") do 525 | if OPERATORS[p] then 526 | lpar = "(" 527 | rpar = ")" 528 | break 529 | end 530 | end 531 | for _, python in ipairs(pythons) do 532 | local python_prefix = rpm.expand("%" .. python .. "_prefix") 533 | print(lpar .. python_prefix .. "-" .. string.gsub(params, "%%python", python_prefix) .. rpar .. " ") 534 | end 535 | end 536 | -------------------------------------------------------------------------------- /macros/001-alternatives: -------------------------------------------------------------------------------- 1 | %prepare_alternative(t:) \ 2 | %define alternative_target %{-t:%{-t*}}%{!-t:%{_bindir}/%1} \ 3 | rm -f %{buildroot}%{alternative_target} \ 4 | alternative_target="%{alternative_target}" \ 5 | if [[ "$alternative_target" == %{_mandir}* ]]; then \ 6 | rm -f %{buildroot}${alternative_target%%%%%{ext_man}} \ 7 | rm -f %{buildroot}%{alternative_target}%{ext_man} \ 8 | fi \ 9 | if [ %{with libalternatives} -eq 1 ] ; then \ 10 | mkdir -p %{buildroot}%{_datadir}/libalternatives \ 11 | if [[ "$alternative_target" != %{_mandir}* ]]; then \ 12 | ln -sf %{_bindir}/alts %{buildroot}%{alternative_target} \ 13 | fi \ 14 | else \ 15 | mkdir -p %{buildroot}%{_sysconfdir}/alternatives \ 16 | touch %{buildroot}%{_sysconfdir}/alternatives/%1 \ 17 | ln -sf %{_sysconfdir}/alternatives/%1 %{buildroot}%{alternative_target} \ 18 | fi \ 19 | %{nil} 20 | 21 | %install_alternative(s:t:p:n:) \ 22 | %define alternative_name %{-n:%{-n*}}%{!-n:%1} \ 23 | %define alternative_source %{-s:%{-s*}}%{!-s:%{_bindir}/%{alternative_name}} \ 24 | %define alternative_target %{-t:%{-t*}}%{!-t:%2} \ 25 | %define alternative_priority %{-p:%{-p*}}%{!-p:%3} \ 26 | update-alternatives --install \\\ 27 | %{alternative_source} \\\ 28 | %{alternative_name} \\\ 29 | %{alternative_target} \\\ 30 | %{alternative_priority} 31 | 32 | %uninstall_alternative(n:t:) \ 33 | %define alternative_name %{-n:%{-n*}}%{!-n:%1} \ 34 | %define alternative_target %{-t:%{-t*}}%{!-t:%2} \ 35 | if [ ! -e "%{alternative_target}" ]; then \ 36 | update-alternatives --quiet --remove "%{alternative_name}" "%{alternative_target}" \ 37 | fi \ 38 | %{nil} 39 | 40 | %reset_alternative(n:t:) \ 41 | %define alternative_name %{-n:%{-n*}}%{!-n:%1} \ 42 | %define alternative_target %{-t:%{-t*}}%{!-t:%2} \ 43 | if [ "$1" -gt 0 ] && [ -f %{_sbindir}/update-alternatives ]; then \ 44 | update-alternatives --quiet --remove "%{alternative_name}" "%{alternative_target}" \ 45 | fi \ 46 | %{nil} 47 | 48 | %alternative_for() \ 49 | %1 \ 50 | %ghost %{_sysconfdir}/alternatives/%{basename:%1} 51 | 52 | %python_flavored_alternatives(v:) \ 53 | %{!-v:set +x} \ 54 | %{python_expand # provide libalternatives and update-alternatives in the current flavor version when shuffling the build dir \ 55 | mkdir -p build/xdgflavorconfig \ 56 | export XDG_CONFIG_HOME=$PWD/build/xdgflavorconfig \ 57 | if [ -d /usr/share/libalternatives/ ]; then \ 58 | for b in /usr/share/libalternatives/*; do \ 59 | if [ -e "${b}/%{$python_version_nodots}.conf" ]; then \ 60 | baseb=$(basename ${b}) \ 61 | alts -n ${baseb} -p %{$python_version_nodots} \ 62 | echo "Using libalternatives variant %{$python_version_nodots} for ${baseb} in XDG_CONFIG_HOME during Python %{$python_version} expansions." \ 63 | fi \ 64 | done \ 65 | fi \ 66 | mkdir -p build/flavorbin \ 67 | for bin in %{_bindir}/*-%{$python_bin_suffix} %{buildroot}%{_bindir}/*-%{$python_bin_suffix}; do \ 68 | if [ -x "${bin}" ]; then \ 69 | # four percent into 1 by rpm/python expansions \ 70 | mainbin="${bin%%%%-%{$python_bin_suffix}}" \ 71 | basemain="$(basename ${mainbin})" \ 72 | if [ "$(readlink ${mainbin})" = "/etc/alternatives/${basemain}" ]; then \ 73 | ln -sf "${bin}" "build/flavorbin/${basemain}" \ 74 | echo "Using alternative $(basename ${bin}) for ${basemain} in ./build/flavorbin during Python %{$python_version} expansions." \ 75 | fi \ 76 | fi \ 77 | done \ 78 | } \ 79 | %{!-v:set -x} \ 80 | export PATH=$PWD/build/flavorbin:$PATH \ 81 | %{nil} 82 | 83 | %python_group_libalternatives() \ 84 | if [ %{with libalternatives} -eq 1 ] ; then \ 85 | group="%{**}" \ 86 | for f in %{buildroot}%{_datadir}/libalternatives/%1/*.conf; do \ 87 | for name in %{**}; do \ 88 | sed -n -i -e '/^group=/!p' -e '$'"a group=${group// /, }" ${f/\\/%{1}\\//\\/$name\\/} \ 89 | done \ 90 | done \ 91 | fi \ 92 | %{nil} 93 | -------------------------------------------------------------------------------- /macros/010-common-defs: -------------------------------------------------------------------------------- 1 | %system_python python2 2 | %python_for_executables python3 3 | 4 | ##### common functionality ##### 5 | 6 | %_python_sysconfig_path() %([ -x %1 ] && RPM_BUILD_ROOT="%{buildroot}" %1 -c "import sysconfig as s; print(s.get_paths().get('%2'))" || echo "!!_{%1}_not_installed_!!") 7 | %_python_sysconfig_var() %([ -x %1 ] && RPM_BUILD_ROOT="%{buildroot}" %1 -c "import sysconfig as s; print(s.get_config_var('%2'))" || echo "!!_{%1}_not_installed_!!") 8 | 9 | %_rec_macro_helper %{lua: 10 | rpm.define("_rec_macro_helper %{nil}") 11 | function expand_macro(name, args) 12 | local pflavor = rpm.expand("%python_flavor") 13 | local args = args and rpm.expand(args) or "" 14 | print(rpm.expand("%{" .. pflavor .. "_" .. name .. " " .. args .."}")) 15 | end 16 | } 17 | 18 | # put wheels into python_expand shuffled wheeldir by default 19 | %_pyproject_wheeldir ./build 20 | 21 | # put flavor-agnostic wheels into a common dir. Dist is the default destination of pytnon -m build 22 | %_pyproject_anywheeldir ./dist 23 | 24 | %pyproject_wheel_args \\\ 25 | --verbose --progress-bar off --disable-pip-version-check \\\ 26 | %{?py_setup_args:--build-option %{py_setup_args}} \\\ 27 | --use-pep517 --no-build-isolation \\\ 28 | --no-deps \\\ 29 | --wheel-dir %{_pyproject_wheeldir} 30 | 31 | %pyproject_install_args \\\ 32 | --verbose --progress-bar off --disable-pip-version-check \\\ 33 | --root %{buildroot} \\\ 34 | --no-compile \\\ 35 | --ignore-installed --no-deps \\\ 36 | --no-index --find-links %{_pyproject_wheeldir} 37 | 38 | ##### fedora compatibility ##### 39 | 40 | %py_setup setup.py 41 | %py_shbang_opts -s 42 | 43 | ##### non-standard binary suffixes for flavors ##### 44 | 45 | %_pypy3_bin_suffix pp%{pypy3_version} 46 | 47 | 48 | ##### preferred configuration ##### 49 | 50 | %python_sitelib %{_python_sysconfig_path %{expand:%__%{python_flavor}} purelib} 51 | %python_sitearch %{_python_sysconfig_path %{expand:%__%{python_flavor}} platlib} 52 | %python_version %{_python_sysconfig_var %{expand:%__%{python_flavor}} py_version_short} 53 | %python_version_nodots %{_python_sysconfig_var %{expand:%__%{python_flavor}} py_version_nodot} 54 | 55 | %python_sysconfig_path() %{_python_sysconfig_path %{expand:%__%{python_flavor}} %1} 56 | %python_sysconfig_var() %{_python_sysconfig_var %{expand:%__%{python_flavor}} %1} 57 | 58 | %python_prefix %{_rec_macro_helper}%{lua:expand_macro("prefix")} 59 | %python_bin_suffix %{_rec_macro_helper}%{lua:expand_macro("bin_suffix")} 60 | %python_provides %{_rec_macro_helper}%{lua:expand_macro("provides")} 61 | 62 | 63 | %python_alternative() %{_rec_macro_helper}%{lua:expand_macro("alternative", "%**")} 64 | %python_install_alternative() %{_rec_macro_helper}%{lua:expand_macro("install_alternative", "%**")} 65 | %python_uninstall_alternative() %{_rec_macro_helper}%{lua:expand_macro("uninstall_alternative", "%**")} 66 | %python_libalternatives_reset_alternative() %{_rec_macro_helper}%{lua:expand_macro("reset_alternative", "%**")} 67 | 68 | # this is by convention hardcoded python2 69 | %py_ver %(python -c "import sys; v=sys.version_info[:2]; print '%%d.%%d'%%v" 2>/dev/null || echo PYTHON-NOT-FOUND) 70 | 71 | ##### Python dependency generator macros ##### 72 | 73 | # === Macros for Build/Requires tags using Python dist tags === 74 | # - https://fedoraproject.org/wiki/Changes/Automatic_Provides_for_Python_RPM_Packages 75 | 76 | # Converts Python dist name to a canonical format 77 | %python_dist_name() %{lua:\ 78 | name = rpm.expand("%{?1:%{1}}");\ 79 | canonical = string.gsub(string.lower(name), "[^%w%.]+", "-");\ 80 | print(canonical);\ 81 | } 82 | 83 | # Creates Python 2 dist tag(s) after converting names to canonical format 84 | # Needs to first put all arguments into a list, because invoking a different 85 | # macro (%python_dist_name) overwrites them 86 | %python2_dist() %{lua:\ 87 | args = {}\ 88 | arg = 1\ 89 | while (true) do\ 90 | name = rpm.expand("%{?" .. arg .. ":%{" .. arg .. "}}");\ 91 | if (name == nil or name == '') then\ 92 | break\ 93 | end\ 94 | args[arg] = name\ 95 | arg = arg + 1\ 96 | end\ 97 | for arg, name in ipairs(args) do\ 98 | canonical = rpm.expand("%python_dist_name " .. name);\ 99 | print("python" .. rpm.expand("%python2_version") .. "dist(" .. canonical .. ") ");\ 100 | end\ 101 | } 102 | 103 | # Creates Python 3 dist tag(s) after converting names to canonical format 104 | # Needs to first put all arguments into a list, because invoking a different 105 | # macro (%python_dist_name) overwrites them 106 | %python3_dist() %{lua:\ 107 | args = {}\ 108 | arg = 1\ 109 | while (true) do\ 110 | name = rpm.expand("%{?" .. arg .. ":%{" .. arg .. "}}");\ 111 | if (name == nil or name == '') then\ 112 | break\ 113 | end\ 114 | args[arg] = name\ 115 | arg = arg + 1\ 116 | end\ 117 | for arg, name in ipairs(args) do\ 118 | canonical = rpm.expand("%python_dist_name " .. name);\ 119 | print("python" .. rpm.expand("%python3_version") .. "dist(" .. canonical .. ") ");\ 120 | end\ 121 | } 122 | 123 | # === Macros to control dependency generator === 124 | # - https://fedoraproject.org/wiki/Changes/EnablingPythonGenerators 125 | %python_enable_dependency_generator() \ 126 | %global __pythondist_requires %{_rpmconfigdir}/pythondistdeps.py --requires \ 127 | %{nil} 128 | 129 | ##### Python Unittest macros ##### 130 | 131 | %pyunittest(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) \ 132 | %python_flavored_alternatives \ 133 | %{lua:\ 134 | local args = rpm.expand("%**"); \ 135 | local broot = rpm.expand("%buildroot"); \ 136 | local intro = "%{python_expand PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}" .. broot .. "%{$python_sitelib} PYTHONDONTWRITEBYTECODE=1 $python -m unittest "; \ 137 | print(rpm.expand(intro .. args .. "}")) \ 138 | } 139 | 140 | %pyunittest_arch(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) \ 141 | %python_flavored_alternatives \ 142 | %{lua:\ 143 | local args = rpm.expand("%**"); \ 144 | local broot = rpm.expand("%buildroot"); \ 145 | local intro = "%{python_expand PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}" .. broot .. "%{$python_sitearch} PYTHONDONTWRITEBYTECODE=1 $python -m unittest "; \ 146 | print(rpm.expand(intro .. args .. "}")) \ 147 | } 148 | 149 | ##### Pytest macros ##### 150 | 151 | %pytest(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) \ 152 | %python_flavored_alternatives \ 153 | %{lua:\ 154 | local args = rpm.expand("%**"); \ 155 | local broot = rpm.expand("%buildroot"); \ 156 | local intro = "%{python_expand PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}" .. broot .. "%{$python_sitelib} PYTHONDONTWRITEBYTECODE=1 "; \ 157 | local ignore_build = "--ignore=_build." .. rpm.expand("%pythons"):gsub("%s+", " --ignore=_build."); \ 158 | intro = intro .. "pytest-%{$python_bin_suffix} " .. ignore_build .. " -v "; \ 159 | print(rpm.expand(intro .. args .. "}")) \ 160 | } 161 | 162 | %pytest_arch(+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=) \ 163 | %python_flavored_alternatives \ 164 | %{lua:\ 165 | local args = rpm.expand("%**"); \ 166 | local broot = rpm.expand("%buildroot"); \ 167 | local intro = "%{python_expand PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}" .. broot .. "%{$python_sitearch} PYTHONDONTWRITEBYTECODE=1 "; \ 168 | local ignore_build = "--ignore=_build." .. rpm.expand("%pythons"):gsub("%s+", " --ignore=_build."); \ 169 | intro = intro .. "pytest-%{$python_bin_suffix} " .. ignore_build .. " -v "; \ 170 | print(rpm.expand(intro .. args .. "}")) \ 171 | } 172 | 173 | ##### Find language files ##### 174 | 175 | %python_find_lang() \ 176 | %find_lang %{**} \ 177 | langfile=%{?2}%{!?2:%1.lang} \ 178 | %{python_expand # \ 179 | grep -v 'python.*site-packages' ${langfile} > %{$python_prefix}-${langfile} \ 180 | grep -F %{$python_sitelib} ${langfile} >> %{$python_prefix}-${langfile} \ 181 | } \ 182 | %{nil} 183 | -------------------------------------------------------------------------------- /macros/030-fallbacks: -------------------------------------------------------------------------------- 1 | ##### compatibility short-name macros ##### 2 | 3 | # fedora expects %py_shbang_opts and %pyX_shbang_opts, possibly to be redefinable? 4 | # we expect everything to start with binary name, so we actually use %pythonX_shbang_opts 5 | # so if a specfile redefines the %pyX_, the correct one will be used 6 | %py2_shbang_opts %py_shbang_opts 7 | %python2_shbang_opts %py2_shbang_opts 8 | %py3_shbang_opts %py_shbang_opts 9 | %python3_shbang_opts %py3_shbang_opts 10 | 11 | %py2_build %python2_build 12 | %py2_install %python2_install 13 | %py3_build %python3_build 14 | %py3_install %python3_install 15 | 16 | %py2_ver %python2_version 17 | %py3_ver %python3_version 18 | 19 | %py_dist_name() %{python_dist_name %{?**}} 20 | %py2_dist() %{python2_dist %{?**}} 21 | %py3_dist() %{python3_dist %{?**}} 22 | 23 | %python2_prefix %{?python2_package_prefix}%{?!python2_package_prefix:python} 24 | -------------------------------------------------------------------------------- /testfiles/fix-shebang.spec: -------------------------------------------------------------------------------- 1 | # 2 | # spec file for package specRPM_CREATION_NAME 3 | # 4 | # Copyright (c) specCURRENT_YEAR SUSE LLC 5 | # 6 | # All modifications and additions to the file contributed by third parties 7 | # remain the property of their copyright owners, unless otherwise agreed 8 | # upon. The license for this file, and modifications and additions to the 9 | # file, is the same license as for the pristine package itself (unless the 10 | # license for the pristine package is not an Open Source License, in which 11 | # case the license is the MIT License). An "Open Source License" is a 12 | # license that conforms to the Open Source Definition (Version 1.9) 13 | # published by the Open Source Initiative. 14 | 15 | # Please submit bugfixes or comments via https://bugs.opensuse.org/ 16 | # 17 | 18 | 19 | Name: fix-shebang 20 | Version: 1.0 21 | Release: 0 22 | Summary: Test fix shebanng 23 | License: GPLv2 24 | URL: https://example.com 25 | BuildRequires: python-rpm-macros 26 | 27 | %description 28 | 29 | %prep 30 | %autosetup 31 | 32 | %build 33 | %configure 34 | %make_build 35 | 36 | %install 37 | %make_install 38 | %python3_fix_shebang_path %{buildroot}/usr/libexec/* 39 | %python3_fix_shebang 40 | 41 | %post 42 | %postun 43 | 44 | %files 45 | %license COPYING 46 | %doc ChangeLog README 47 | 48 | %changelog 49 | 50 | --------------------------------------------------------------------------------