python (3.11.7)

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