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