340527b08a24a4376a828cebc9dadaf02fb76a04
[SubU] /
1 """distutils.command.bdist_rpm
2
3 Implements the Distutils 'bdist_rpm' command (create RPM source and binary
4 distributions)."""
5
6 import subprocess
7 import sys
8 import os
9
10 from ..core import Command
11 from ..debug import DEBUG
12 from ..file_util import write_file
13 from ..errors import (
14     DistutilsOptionError,
15     DistutilsPlatformError,
16     DistutilsFileError,
17     DistutilsExecError,
18 )
19 from ..sysconfig import get_python_version
20 from distutils._log import log
21
22
23 class bdist_rpm(Command):
24
25     description = "create an RPM distribution"
26
27     user_options = [
28         ('bdist-base=', None, "base directory for creating built distributions"),
29         (
30             'rpm-base=',
31             None,
32             "base directory for creating RPMs (defaults to \"rpm\" under "
33             "--bdist-base; must be specified for RPM 2)",
34         ),
35         (
36             'dist-dir=',
37             'd',
38             "directory to put final RPM files in " "(and .spec files if --spec-only)",
39         ),
40         (
41             'python=',
42             None,
43             "path to Python interpreter to hard-code in the .spec file "
44             "(default: \"python\")",
45         ),
46         (
47             'fix-python',
48             None,
49             "hard-code the exact path to the current Python interpreter in "
50             "the .spec file",
51         ),
52         ('spec-only', None, "only regenerate spec file"),
53         ('source-only', None, "only generate source RPM"),
54         ('binary-only', None, "only generate binary RPM"),
55         ('use-bzip2', None, "use bzip2 instead of gzip to create source distribution"),
56         # More meta-data: too RPM-specific to put in the setup script,
57         # but needs to go in the .spec file -- so we make these options
58         # to "bdist_rpm".  The idea is that packagers would put this
59         # info in setup.cfg, although they are of course free to
60         # supply it on the command line.
61         (
62             'distribution-name=',
63             None,
64             "name of the (Linux) distribution to which this "
65             "RPM applies (*not* the name of the module distribution!)",
66         ),
67         ('group=', None, "package classification [default: \"Development/Libraries\"]"),
68         ('release=', None, "RPM release number"),
69         ('serial=', None, "RPM serial number"),
70         (
71             'vendor=',
72             None,
73             "RPM \"vendor\" (eg. \"Joe Blow <joe@example.com>\") "
74             "[default: maintainer or author from setup script]",
75         ),
76         (
77             'packager=',
78             None,
79             "RPM packager (eg. \"Jane Doe <jane@example.net>\") " "[default: vendor]",
80         ),
81         ('doc-files=', None, "list of documentation files (space or comma-separated)"),
82         ('changelog=', None, "RPM changelog"),
83         ('icon=', None, "name of icon file"),
84         ('provides=', None, "capabilities provided by this package"),
85         ('requires=', None, "capabilities required by this package"),
86         ('conflicts=', None, "capabilities which conflict with this package"),
87         ('build-requires=', None, "capabilities required to build this package"),
88         ('obsoletes=', None, "capabilities made obsolete by this package"),
89         ('no-autoreq', None, "do not automatically calculate dependencies"),
90         # Actions to take when building RPM
91         ('keep-temp', 'k', "don't clean up RPM build directory"),
92         ('no-keep-temp', None, "clean up RPM build directory [default]"),
93         (
94             'use-rpm-opt-flags',
95             None,
96             "compile with RPM_OPT_FLAGS when building from source RPM",
97         ),
98         ('no-rpm-opt-flags', None, "do not pass any RPM CFLAGS to compiler"),
99         ('rpm3-mode', None, "RPM 3 compatibility mode (default)"),
100         ('rpm2-mode', None, "RPM 2 compatibility mode"),
101         # Add the hooks necessary for specifying custom scripts
102         ('prep-script=', None, "Specify a script for the PREP phase of RPM building"),
103         ('build-script=', None, "Specify a script for the BUILD phase of RPM building"),
104         (
105             'pre-install=',
106             None,
107             "Specify a script for the pre-INSTALL phase of RPM building",
108         ),
109         (
110             'install-script=',
111             None,
112             "Specify a script for the INSTALL phase of RPM building",
113         ),
114         (
115             'post-install=',
116             None,
117             "Specify a script for the post-INSTALL phase of RPM building",
118         ),
119         (
120             'pre-uninstall=',
121             None,
122             "Specify a script for the pre-UNINSTALL phase of RPM building",
123         ),
124         (
125             'post-uninstall=',
126             None,
127             "Specify a script for the post-UNINSTALL phase of RPM building",
128         ),
129         ('clean-script=', None, "Specify a script for the CLEAN phase of RPM building"),
130         (
131             'verify-script=',
132             None,
133             "Specify a script for the VERIFY phase of the RPM build",
134         ),
135         # Allow a packager to explicitly force an architecture
136         ('force-arch=', None, "Force an architecture onto the RPM build process"),
137         ('quiet', 'q', "Run the INSTALL phase of RPM building in quiet mode"),
138     ]
139
140     boolean_options = [
141         'keep-temp',
142         'use-rpm-opt-flags',
143         'rpm3-mode',
144         'no-autoreq',
145         'quiet',
146     ]
147
148     negative_opt = {
149         'no-keep-temp': 'keep-temp',
150         'no-rpm-opt-flags': 'use-rpm-opt-flags',
151         'rpm2-mode': 'rpm3-mode',
152     }
153
154     def initialize_options(self):
155         self.bdist_base = None
156         self.rpm_base = None
157         self.dist_dir = None
158         self.python = None
159         self.fix_python = None
160         self.spec_only = None
161         self.binary_only = None
162         self.source_only = None
163         self.use_bzip2 = None
164
165         self.distribution_name = None
166         self.group = None
167         self.release = None
168         self.serial = None
169         self.vendor = None
170         self.packager = None
171         self.doc_files = None
172         self.changelog = None
173         self.icon = None
174
175         self.prep_script = None
176         self.build_script = None
177         self.install_script = None
178         self.clean_script = None
179         self.verify_script = None
180         self.pre_install = None
181         self.post_install = None
182         self.pre_uninstall = None
183         self.post_uninstall = None
184         self.prep = None
185         self.provides = None
186         self.requires = None
187         self.conflicts = None
188         self.build_requires = None
189         self.obsoletes = None
190
191         self.keep_temp = 0
192         self.use_rpm_opt_flags = 1
193         self.rpm3_mode = 1
194         self.no_autoreq = 0
195
196         self.force_arch = None
197         self.quiet = 0
198
199     def finalize_options(self):
200         self.set_undefined_options('bdist', ('bdist_base', 'bdist_base'))
201         if self.rpm_base is None:
202             if not self.rpm3_mode:
203                 raise DistutilsOptionError("you must specify --rpm-base in RPM 2 mode")
204             self.rpm_base = os.path.join(self.bdist_base, "rpm")
205
206         if self.python is None:
207             if self.fix_python:
208                 self.python = sys.executable
209             else:
210                 self.python = "python3"
211         elif self.fix_python:
212             raise DistutilsOptionError(
213                 "--python and --fix-python are mutually exclusive options"
214             )
215
216         if os.name != 'posix':
217             raise DistutilsPlatformError(
218                 "don't know how to create RPM " "distributions on platform %s" % os.name
219             )
220         if self.binary_only and self.source_only:
221             raise DistutilsOptionError(
222                 "cannot supply both '--source-only' and '--binary-only'"
223             )
224
225         # don't pass CFLAGS to pure python distributions
226         if not self.distribution.has_ext_modules():
227             self.use_rpm_opt_flags = 0
228
229         self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
230         self.finalize_package_data()
231
232     def finalize_package_data(self):
233         self.ensure_string('group', "Development/Libraries")
234         self.ensure_string(
235             'vendor',
236             "%s <%s>"
237             % (self.distribution.get_contact(), self.distribution.get_contact_email()),
238         )
239         self.ensure_string('packager')
240         self.ensure_string_list('doc_files')
241         if isinstance(self.doc_files, list):
242             for readme in ('README', 'README.txt'):
243                 if os.path.exists(readme) and readme not in self.doc_files:
244                     self.doc_files.append(readme)
245
246         self.ensure_string('release', "1")
247         self.ensure_string('serial')  # should it be an int?
248
249         self.ensure_string('distribution_name')
250
251         self.ensure_string('changelog')
252         # Format changelog correctly
253         self.changelog = self._format_changelog(self.changelog)
254
255         self.ensure_filename('icon')
256
257         self.ensure_filename('prep_script')
258         self.ensure_filename('build_script')
259         self.ensure_filename('install_script')
260         self.ensure_filename('clean_script')
261         self.ensure_filename('verify_script')
262         self.ensure_filename('pre_install')
263         self.ensure_filename('post_install')
264         self.ensure_filename('pre_uninstall')
265         self.ensure_filename('post_uninstall')
266
267         # XXX don't forget we punted on summaries and descriptions -- they
268         # should be handled here eventually!
269
270         # Now *this* is some meta-data that belongs in the setup script...
271         self.ensure_string_list('provides')
272         self.ensure_string_list('requires')
273         self.ensure_string_list('conflicts')
274         self.ensure_string_list('build_requires')
275         self.ensure_string_list('obsoletes')
276
277         self.ensure_string('force_arch')
278
279     def run(self):  # noqa: C901
280         if DEBUG:
281             print("before _get_package_data():")
282             print("vendor =", self.vendor)
283             print("packager =", self.packager)
284             print("doc_files =", self.doc_files)
285             print("changelog =", self.changelog)
286
287         # make directories
288         if self.spec_only:
289             spec_dir = self.dist_dir
290             self.mkpath(spec_dir)
291         else:
292             rpm_dir = {}
293             for d in ('SOURCES', 'SPECS', 'BUILD', 'RPMS', 'SRPMS'):
294                 rpm_dir[d] = os.path.join(self.rpm_base, d)
295                 self.mkpath(rpm_dir[d])
296             spec_dir = rpm_dir['SPECS']
297
298         # Spec file goes into 'dist_dir' if '--spec-only specified',
299         # build/rpm.<plat> otherwise.
300         spec_path = os.path.join(spec_dir, "%s.spec" % self.distribution.get_name())
301         self.execute(
302             write_file, (spec_path, self._make_spec_file()), "writing '%s'" % spec_path
303         )
304
305         if self.spec_only:  # stop if requested
306             return
307
308         # Make a source distribution and copy to SOURCES directory with
309         # optional icon.
310         saved_dist_files = self.distribution.dist_files[:]
311         sdist = self.reinitialize_command('sdist')
312         if self.use_bzip2:
313             sdist.formats = ['bztar']
314         else:
315             sdist.formats = ['gztar']
316         self.run_command('sdist')
317         self.distribution.dist_files = saved_dist_files
318
319         source = sdist.get_archive_files()[0]
320         source_dir = rpm_dir['SOURCES']
321         self.copy_file(source, source_dir)
322
323         if self.icon:
324             if os.path.exists(self.icon):
325                 self.copy_file(self.icon, source_dir)
326             else:
327                 raise DistutilsFileError("icon file '%s' does not exist" % self.icon)
328
329         # build package
330         log.info("building RPMs")
331         rpm_cmd = ['rpmbuild']
332
333         if self.source_only:  # what kind of RPMs?
334             rpm_cmd.append('-bs')
335         elif self.binary_only:
336             rpm_cmd.append('-bb')
337         else:
338             rpm_cmd.append('-ba')
339         rpm_cmd.extend(['--define', '__python %s' % self.python])
340         if self.rpm3_mode:
341             rpm_cmd.extend(['--define', '_topdir %s' % os.path.abspath(self.rpm_base)])
342         if not self.keep_temp:
343             rpm_cmd.append('--clean')
344
345         if self.quiet:
346             rpm_cmd.append('--quiet')
347
348         rpm_cmd.append(spec_path)
349         # Determine the binary rpm names that should be built out of this spec
350         # file
351         # Note that some of these may not be really built (if the file
352         # list is empty)
353         nvr_string = "%{name}-%{version}-%{release}"
354         src_rpm = nvr_string + ".src.rpm"
355         non_src_rpm = "%{arch}/" + nvr_string + ".%{arch}.rpm"
356         q_cmd = r"rpm -q --qf '{} {}\n' --specfile '{}'".format(
357             src_rpm,
358             non_src_rpm,
359             spec_path,
360         )
361
362         out = os.popen(q_cmd)
363         try:
364             binary_rpms = []
365             source_rpm = None
366             while True:
367                 line = out.readline()
368                 if not line:
369                     break
370                 ell = line.strip().split()
371                 assert len(ell) == 2
372                 binary_rpms.append(ell[1])
373                 # The source rpm is named after the first entry in the spec file
374                 if source_rpm is None:
375                     source_rpm = ell[0]
376
377             status = out.close()
378             if status:
379                 raise DistutilsExecError("Failed to execute: %s" % repr(q_cmd))
380
381         finally:
382             out.close()
383
384         self.spawn(rpm_cmd)
385
386         if not self.dry_run:
387             if self.distribution.has_ext_modules():
388                 pyversion = get_python_version()
389             else:
390                 pyversion = 'any'
391
392             if not self.binary_only:
393                 srpm = os.path.join(rpm_dir['SRPMS'], source_rpm)
394                 assert os.path.exists(srpm)
395                 self.move_file(srpm, self.dist_dir)
396                 filename = os.path.join(self.dist_dir, source_rpm)
397                 self.distribution.dist_files.append(('bdist_rpm', pyversion, filename))
398
399             if not self.source_only:
400                 for rpm in binary_rpms:
401                     rpm = os.path.join(rpm_dir['RPMS'], rpm)
402                     if os.path.exists(rpm):
403                         self.move_file(rpm, self.dist_dir)
404                         filename = os.path.join(self.dist_dir, os.path.basename(rpm))
405                         self.distribution.dist_files.append(
406                             ('bdist_rpm', pyversion, filename)
407                         )
408
409     def _dist_path(self, path):
410         return os.path.join(self.dist_dir, os.path.basename(path))
411
412     def _make_spec_file(self):  # noqa: C901
413         """Generate the text of an RPM spec file and return it as a
414         list of strings (one per line).
415         """
416         # definitions and headers
417         spec_file = [
418             '%define name ' + self.distribution.get_name(),
419             '%define version ' + self.distribution.get_version().replace('-', '_'),
420             '%define unmangled_version ' + self.distribution.get_version(),
421             '%define release ' + self.release.replace('-', '_'),
422             '',
423             'Summary: ' + (self.distribution.get_description() or "UNKNOWN"),
424         ]
425
426         # Workaround for #14443 which affects some RPM based systems such as
427         # RHEL6 (and probably derivatives)
428         vendor_hook = subprocess.getoutput('rpm --eval %{__os_install_post}')
429         # Generate a potential replacement value for __os_install_post (whilst
430         # normalizing the whitespace to simplify the test for whether the
431         # invocation of brp-python-bytecompile passes in __python):
432         vendor_hook = '\n'.join(
433             ['  %s \\' % line.strip() for line in vendor_hook.splitlines()]
434         )
435         problem = "brp-python-bytecompile \\\n"
436         fixed = "brp-python-bytecompile %{__python} \\\n"
437         fixed_hook = vendor_hook.replace(problem, fixed)
438         if fixed_hook != vendor_hook:
439             spec_file.append('# Workaround for http://bugs.python.org/issue14443')
440             spec_file.append('%define __os_install_post ' + fixed_hook + '\n')
441
442         # put locale summaries into spec file
443         # XXX not supported for now (hard to put a dictionary
444         # in a config file -- arg!)
445         # for locale in self.summaries.keys():
446         #    spec_file.append('Summary(%s): %s' % (locale,
447         #                                          self.summaries[locale]))
448
449         spec_file.extend(
450             [
451                 'Name: %{name}',
452                 'Version: %{version}',
453                 'Release: %{release}',
454             ]
455         )
456
457         # XXX yuck! this filename is available from the "sdist" command,
458         # but only after it has run: and we create the spec file before
459         # running "sdist", in case of --spec-only.
460         if self.use_bzip2:
461             spec_file.append('Source0: %{name}-%{unmangled_version}.tar.bz2')
462         else:
463             spec_file.append('Source0: %{name}-%{unmangled_version}.tar.gz')
464
465         spec_file.extend(
466             [
467                 'License: ' + (self.distribution.get_license() or "UNKNOWN"),
468                 'Group: ' + self.group,
469                 'BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot',
470                 'Prefix: %{_prefix}',
471             ]
472         )
473
474         if not self.force_arch:
475             # noarch if no extension modules
476             if not self.distribution.has_ext_modules():
477                 spec_file.append('BuildArch: noarch')
478         else:
479             spec_file.append('BuildArch: %s' % self.force_arch)
480
481         for field in (
482             'Vendor',
483             'Packager',
484             'Provides',
485             'Requires',
486             'Conflicts',
487             'Obsoletes',
488         ):
489             val = getattr(self, field.lower())
490             if isinstance(val, list):
491                 spec_file.append('{}: {}'.format(field, ' '.join(val)))
492             elif val is not None:
493                 spec_file.append('{}: {}'.format(field, val))
494
495         if self.distribution.get_url():
496             spec_file.append('Url: ' + self.distribution.get_url())
497
498         if self.distribution_name:
499             spec_file.append('Distribution: ' + self.distribution_name)
500
501         if self.build_requires:
502             spec_file.append('BuildRequires: ' + ' '.join(self.build_requires))
503
504         if self.icon:
505             spec_file.append('Icon: ' + os.path.basename(self.icon))
506
507         if self.no_autoreq:
508             spec_file.append('AutoReq: 0')
509
510         spec_file.extend(
511             [
512                 '',
513                 '%description',
514                 self.distribution.get_long_description() or "",
515             ]
516         )
517
518         # put locale descriptions into spec file
519         # XXX again, suppressed because config file syntax doesn't
520         # easily support this ;-(
521         # for locale in self.descriptions.keys():
522         #    spec_file.extend([
523         #        '',
524         #        '%description -l ' + locale,
525         #        self.descriptions[locale],
526         #        ])
527
528         # rpm scripts
529         # figure out default build script
530         def_setup_call = "{} {}".format(self.python, os.path.basename(sys.argv[0]))
531         def_build = "%s build" % def_setup_call
532         if self.use_rpm_opt_flags:
533             def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build
534
535         # insert contents of files
536
537         # XXX this is kind of misleading: user-supplied options are files
538         # that we open and interpolate into the spec file, but the defaults
539         # are just text that we drop in as-is.  Hmmm.
540
541         install_cmd = (
542             '%s install -O1 --root=$RPM_BUILD_ROOT ' '--record=INSTALLED_FILES'
543         ) % def_setup_call
544
545         script_options = [
546             ('prep', 'prep_script', "%setup -n %{name}-%{unmangled_version}"),
547             ('build', 'build_script', def_build),
548             ('install', 'install_script', install_cmd),
549             ('clean', 'clean_script', "rm -rf $RPM_BUILD_ROOT"),
550             ('verifyscript', 'verify_script', None),
551             ('pre', 'pre_install', None),
552             ('post', 'post_install', None),
553             ('preun', 'pre_uninstall', None),
554             ('postun', 'post_uninstall', None),
555         ]
556
557         for (rpm_opt, attr, default) in script_options:
558             # Insert contents of file referred to, if no file is referred to
559             # use 'default' as contents of script
560             val = getattr(self, attr)
561             if val or default:
562                 spec_file.extend(
563                     [
564                         '',
565                         '%' + rpm_opt,
566                     ]
567                 )
568                 if val:
569                     with open(val) as f:
570                         spec_file.extend(f.read().split('\n'))
571                 else:
572                     spec_file.append(default)
573
574         # files section
575         spec_file.extend(
576             [
577                 '',
578                 '%files -f INSTALLED_FILES',
579                 '%defattr(-,root,root)',
580             ]
581         )
582
583         if self.doc_files:
584             spec_file.append('%doc ' + ' '.join(self.doc_files))
585
586         if self.changelog:
587             spec_file.extend(
588                 [
589                     '',
590                     '%changelog',
591                 ]
592             )
593             spec_file.extend(self.changelog)
594
595         return spec_file
596
597     def _format_changelog(self, changelog):
598         """Format the changelog correctly and convert it to a list of strings"""
599         if not changelog:
600             return changelog
601         new_changelog = []
602         for line in changelog.strip().split('\n'):
603             line = line.strip()
604             if line[0] == '*':
605                 new_changelog.extend(['', line])
606             elif line[0] == '-':
607                 new_changelog.append(line)
608             else:
609                 new_changelog.append('  ' + line)
610
611         # strip trailing newline inserted by first changelog entry
612         if not new_changelog[0]:
613             del new_changelog[0]
614
615         return new_changelog