Home | History | Annotate | Download | only in command
      1 """distutils.command.bdist_rpm
      2 
      3 Implements the Distutils 'bdist_rpm' command (create RPM source and binary
      4 distributions)."""
      5 
      6 __revision__ = "$Id$"
      7 
      8 import sys
      9 import os
     10 import string
     11 
     12 from distutils.core import Command
     13 from distutils.debug import DEBUG
     14 from distutils.file_util import write_file
     15 from distutils.errors import (DistutilsOptionError, DistutilsPlatformError,
     16                               DistutilsFileError, DistutilsExecError)
     17 from distutils import log
     18 
     19 class 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 (at] example.com>\") "
     63          "[default: maintainer or author from setup script]"),
     64         ('packager=', None,
     65          "RPM packager (eg. \"Jane Doe <jane (at] 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