1  """distutils.command.bdist_rpm
       2  
       3  Implements the Distutils 'bdist_rpm' command (create RPM source and binary
       4  distributions)."""
       5  
       6  import subprocess, sys, os
       7  from distutils.core import Command
       8  from distutils.debug import DEBUG
       9  from distutils.file_util import write_file
      10  from distutils.errors import *
      11  from distutils.sysconfig import get_python_version
      12  from distutils import log
      13  
      14  class ESC[4;38;5;81mbdist_rpm(ESC[4;38;5;149mCommand):
      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