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