codekingpro/portable-devtools
114k
1"""distutils.command.bdist_rpm2 3Implements the Distutils 'bdist_rpm' command (create RPM source and binary4distributions)."""5 6import os7import subprocess8import sys9from distutils._log import log10 11from ..core import Command12from ..debug import DEBUG13from ..errors import (14 DistutilsExecError,15 DistutilsFileError,16 DistutilsOptionError,17 DistutilsPlatformError,18)19from ..file_util import write_file20from ..sysconfig import get_python_version21 22 23class bdist_rpm(Command):24 description = "create an RPM distribution"25 26 user_options = [27 ('bdist-base=', None, "base directory for creating built distributions"),28 (29 'rpm-base=',30 None,31 "base directory for creating RPMs (defaults to \"rpm\" under "32 "--bdist-base; must be specified for RPM 2)",33 ),34 (35 'dist-dir=',36 'd',37 "directory to put final RPM files in (and .spec files if --spec-only)",38 ),39 (40 'python=',41 None,42 "path to Python interpreter to hard-code in the .spec file "43 "(default: \"python\")",44 ),45 (46 'fix-python',47 None,48 "hard-code the exact path to the current Python interpreter in "49 "the .spec file",50 ),51 ('spec-only', None, "only regenerate spec file"),52 ('source-only', None, "only generate source RPM"),53 ('binary-only', None, "only generate binary RPM"),54 ('use-bzip2', None, "use bzip2 instead of gzip to create source distribution"),55 # More meta-data: too RPM-specific to put in the setup script,56 # but needs to go in the .spec file -- so we make these options57 # to "bdist_rpm". The idea is that packagers would put this58 # info in setup.cfg, although they are of course free to59 # supply it on the command line.60 (61 'distribution-name=',62 None,63 "name of the (Linux) distribution to which this "64 "RPM applies (*not* the name of the module distribution!)",65 ),66 ('group=', None, "package classification [default: \"Development/Libraries\"]"),67 ('release=', None, "RPM release number"),68 ('serial=', None, "RPM serial number"),69 (70 'vendor=',71 None,72 "RPM \"vendor\" (eg. \"Joe Blow <joe@example.com>\") "73 "[default: maintainer or author from setup script]",74 ),75 (76 'packager=',77 None,78 "RPM packager (eg. \"Jane Doe <jane@example.net>\") [default: vendor]",79 ),80 ('doc-files=', None, "list of documentation files (space or comma-separated)"),81 ('changelog=', None, "RPM changelog"),82 ('icon=', None, "name of icon file"),83 ('provides=', None, "capabilities provided by this package"),84 ('requires=', None, "capabilities required by this package"),85 ('conflicts=', None, "capabilities which conflict with this package"),86 ('build-requires=', None, "capabilities required to build this package"),87 ('obsoletes=', None, "capabilities made obsolete by this package"),88 ('no-autoreq', None, "do not automatically calculate dependencies"),89 # Actions to take when building RPM90 ('keep-temp', 'k', "don't clean up RPM build directory"),91 ('no-keep-temp', None, "clean up RPM build directory [default]"),92 (93 'use-rpm-opt-flags',94 None,95 "compile with RPM_OPT_FLAGS when building from source RPM",96 ),97 ('no-rpm-opt-flags', None, "do not pass any RPM CFLAGS to compiler"),98 ('rpm3-mode', None, "RPM 3 compatibility mode (default)"),99 ('rpm2-mode', None, "RPM 2 compatibility mode"),100 # Add the hooks necessary for specifying custom scripts101 ('prep-script=', None, "Specify a script for the PREP phase of RPM building"),102 ('build-script=', None, "Specify a script for the BUILD phase of RPM building"),103 (104 'pre-install=',105 None,106 "Specify a script for the pre-INSTALL phase of RPM building",107 ),108 (109 'install-script=',110 None,111 "Specify a script for the INSTALL phase of RPM building",112 ),113 (114 'post-install=',115 None,116 "Specify a script for the post-INSTALL phase of RPM building",117 ),118 (119 'pre-uninstall=',120 None,121 "Specify a script for the pre-UNINSTALL phase of RPM building",122 ),123 (124 'post-uninstall=',125 None,126 "Specify a script for the post-UNINSTALL phase of RPM building",127 ),128 ('clean-script=', None, "Specify a script for the CLEAN phase of RPM building"),129 (130 'verify-script=',131 None,132 "Specify a script for the VERIFY phase of the RPM build",133 ),134 # Allow a packager to explicitly force an architecture135 ('force-arch=', None, "Force an architecture onto the RPM build process"),136 ('quiet', 'q', "Run the INSTALL phase of RPM building in quiet mode"),137 ]138 139 boolean_options = [140 'keep-temp',141 'use-rpm-opt-flags',142 'rpm3-mode',143 'no-autoreq',144 'quiet',145 ]146 147 negative_opt = {148 'no-keep-temp': 'keep-temp',149 'no-rpm-opt-flags': 'use-rpm-opt-flags',150 'rpm2-mode': 'rpm3-mode',151 }152 153 def initialize_options(self):154 self.bdist_base = None155 self.rpm_base = None156 self.dist_dir = None157 self.python = None158 self.fix_python = None159 self.spec_only = None160 self.binary_only = None161 self.source_only = None162 self.use_bzip2 = None163 164 self.distribution_name = None165 self.group = None166 self.release = None167 self.serial = None168 self.vendor = None169 self.packager = None170 self.doc_files = None171 self.changelog = None172 self.icon = None173 174 self.prep_script = None175 self.build_script = None176 self.install_script = None177 self.clean_script = None178 self.verify_script = None179 self.pre_install = None180 self.post_install = None181 self.pre_uninstall = None182 self.post_uninstall = None183 self.prep = None184 self.provides = None185 self.requires = None186 self.conflicts = None187 self.build_requires = None188 self.obsoletes = None189 190 self.keep_temp = 0191 self.use_rpm_opt_flags = 1192 self.rpm3_mode = 1193 self.no_autoreq = 0194 195 self.force_arch = None196 self.quiet = 0197 198 def finalize_options(self):199 self.set_undefined_options('bdist', ('bdist_base', 'bdist_base'))200 if self.rpm_base is None:201 if not self.rpm3_mode:202 raise DistutilsOptionError("you must specify --rpm-base in RPM 2 mode")203 self.rpm_base = os.path.join(self.bdist_base, "rpm")204 205 if self.python is None:206 if self.fix_python:207 self.python = sys.executable208 else:209 self.python = "python3"210 elif self.fix_python:211 raise DistutilsOptionError(212 "--python and --fix-python are mutually exclusive options"213 )214 215 if os.name != 'posix':216 raise DistutilsPlatformError(217 "don't know how to create RPM distributions on platform %s" % os.name218 )219 if self.binary_only and self.source_only:220 raise DistutilsOptionError(221 "cannot supply both '--source-only' and '--binary-only'"222 )223 224 # don't pass CFLAGS to pure python distributions225 if not self.distribution.has_ext_modules():226 self.use_rpm_opt_flags = 0227 228 self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))229 self.finalize_package_data()230 231 def finalize_package_data(self):232 self.ensure_string('group', "Development/Libraries")233 self.ensure_string(234 'vendor',235 f"{self.distribution.get_contact()} <{self.distribution.get_contact_email()}>",236 )237 self.ensure_string('packager')238 self.ensure_string_list('doc_files')239 if isinstance(self.doc_files, list):240 for readme in ('README', 'README.txt'):241 if os.path.exists(readme) and readme not in self.doc_files:242 self.doc_files.append(readme)243 244 self.ensure_string('release', "1")245 self.ensure_string('serial') # should it be an int?246 247 self.ensure_string('distribution_name')248 249 self.ensure_string('changelog')250 # Format changelog correctly251 self.changelog = self._format_changelog(self.changelog)252 253 self.ensure_filename('icon')254 255 self.ensure_filename('prep_script')256 self.ensure_filename('build_script')257 self.ensure_filename('install_script')258 self.ensure_filename('clean_script')259 self.ensure_filename('verify_script')260 self.ensure_filename('pre_install')261 self.ensure_filename('post_install')262 self.ensure_filename('pre_uninstall')263 self.ensure_filename('post_uninstall')264 265 # XXX don't forget we punted on summaries and descriptions -- they266 # should be handled here eventually!267 268 # Now *this* is some meta-data that belongs in the setup script...269 self.ensure_string_list('provides')270 self.ensure_string_list('requires')271 self.ensure_string_list('conflicts')272 self.ensure_string_list('build_requires')273 self.ensure_string_list('obsoletes')274 275 self.ensure_string('force_arch')276 277 def run(self): # noqa: C901278 if DEBUG:279 print("before _get_package_data():")280 print("vendor =", self.vendor)281 print("packager =", self.packager)282 print("doc_files =", self.doc_files)283 print("changelog =", self.changelog)284 285 # make directories286 if self.spec_only:287 spec_dir = self.dist_dir288 self.mkpath(spec_dir)289 else:290 rpm_dir = {}291 for d in ('SOURCES', 'SPECS', 'BUILD', 'RPMS', 'SRPMS'):292 rpm_dir[d] = os.path.join(self.rpm_base, d)293 self.mkpath(rpm_dir[d])294 spec_dir = rpm_dir['SPECS']295 296 # Spec file goes into 'dist_dir' if '--spec-only specified',297 # build/rpm.<plat> otherwise.298 spec_path = os.path.join(spec_dir, "%s.spec" % self.distribution.get_name())299 self.execute(300 write_file, (spec_path, self._make_spec_file()), "writing '%s'" % spec_path301 )302 303 if self.spec_only: # stop if requested304 return305 306 # Make a source distribution and copy to SOURCES directory with307 # optional icon.308 saved_dist_files = self.distribution.dist_files[:]309 sdist = self.reinitialize_command('sdist')310 if self.use_bzip2:311 sdist.formats = ['bztar']312 else:313 sdist.formats = ['gztar']314 self.run_command('sdist')315 self.distribution.dist_files = saved_dist_files316 317 source = sdist.get_archive_files()[0]318 source_dir = rpm_dir['SOURCES']319 self.copy_file(source, source_dir)320 321 if self.icon:322 if os.path.exists(self.icon):323 self.copy_file(self.icon, source_dir)324 else:325 raise DistutilsFileError("icon file '%s' does not exist" % self.icon)326 327 # build package328 log.info("building RPMs")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 rpm_cmd.extend(['--define', '__python %s' % self.python])338 if self.rpm3_mode:339 rpm_cmd.extend(['--define', '_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 spec348 # file349 # Note that some of these may not be really built (if the file350 # 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 = rf"rpm -q --qf '{src_rpm} {non_src_rpm}\n' --specfile '{spec_path}'"355 356 out = os.popen(q_cmd)357 try:358 binary_rpms = []359 source_rpm = None360 while True:361 line = out.readline()362 if not line:363 break364 ell = line.strip().split()365 assert len(ell) == 2366 binary_rpms.append(ell[1])367 # The source rpm is named after the first entry in the spec file368 if source_rpm is None:369 source_rpm = ell[0]370 371 status = out.close()372 if status:373 raise DistutilsExecError("Failed to execute: %s" % repr(q_cmd))374 375 finally:376 out.close()377 378 self.spawn(rpm_cmd)379 380 if not self.dry_run:381 if self.distribution.has_ext_modules():382 pyversion = get_python_version()383 else:384 pyversion = 'any'385 386 if not self.binary_only:387 srpm = os.path.join(rpm_dir['SRPMS'], source_rpm)388 assert os.path.exists(srpm)389 self.move_file(srpm, self.dist_dir)390 filename = os.path.join(self.dist_dir, source_rpm)391 self.distribution.dist_files.append(('bdist_rpm', pyversion, filename))392 393 if not self.source_only:394 for rpm in binary_rpms:395 rpm = os.path.join(rpm_dir['RPMS'], rpm)396 if os.path.exists(rpm):397 self.move_file(rpm, self.dist_dir)398 filename = os.path.join(self.dist_dir, os.path.basename(rpm))399 self.distribution.dist_files.append((400 'bdist_rpm',401 pyversion,402 filename,403 ))404 405 def _dist_path(self, path):406 return os.path.join(self.dist_dir, os.path.basename(path))407 408 def _make_spec_file(self): # noqa: C901409 """Generate the text of an RPM spec file and return it as a410 list of strings (one per line).411 """412 # definitions and headers413 spec_file = [414 '%define name ' + self.distribution.get_name(),415 '%define version ' + self.distribution.get_version().replace('-', '_'),416 '%define unmangled_version ' + self.distribution.get_version(),417 '%define release ' + self.release.replace('-', '_'),418 '',419 'Summary: ' + (self.distribution.get_description() or "UNKNOWN"),420 ]421 422 # Workaround for #14443 which affects some RPM based systems such as423 # RHEL6 (and probably derivatives)424 vendor_hook = subprocess.getoutput('rpm --eval %{__os_install_post}')425 # Generate a potential replacement value for __os_install_post (whilst426 # normalizing the whitespace to simplify the test for whether the427 # invocation of brp-python-bytecompile passes in __python):428 vendor_hook = '\n'.join([429 ' %s \\' % line.strip() for line in vendor_hook.splitlines()430 ])431 problem = "brp-python-bytecompile \\\n"432 fixed = "brp-python-bytecompile %{__python} \\\n"433 fixed_hook = vendor_hook.replace(problem, fixed)434 if fixed_hook != vendor_hook:435 spec_file.append('# Workaround for https://bugs.python.org/issue14443')436 spec_file.append('%define __os_install_post ' + fixed_hook + '\n')437 438 # put locale summaries into spec file439 # XXX not supported for now (hard to put a dictionary440 # in a config file -- arg!)441 # for locale in self.summaries.keys():442 # spec_file.append('Summary(%s): %s' % (locale,443 # self.summaries[locale]))444 445 spec_file.extend([446 'Name: %{name}',447 'Version: %{version}',448 'Release: %{release}',449 ])450 451 # XXX yuck! this filename is available from the "sdist" command,452 # but only after it has run: and we create the spec file before453 # running "sdist", in case of --spec-only.454 if self.use_bzip2:455 spec_file.append('Source0: %{name}-%{unmangled_version}.tar.bz2')456 else:457 spec_file.append('Source0: %{name}-%{unmangled_version}.tar.gz')458 459 spec_file.extend([460 'License: ' + (self.distribution.get_license() or "UNKNOWN"),461 'Group: ' + self.group,462 'BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot',463 'Prefix: %{_prefix}',464 ])465 466 if not self.force_arch:467 # noarch if no extension modules468 if not self.distribution.has_ext_modules():469 spec_file.append('BuildArch: noarch')470 else:471 spec_file.append('BuildArch: %s' % self.force_arch)472 473 for field in (474 'Vendor',475 'Packager',476 'Provides',477 'Requires',478 'Conflicts',479 'Obsoletes',480 ):481 val = getattr(self, field.lower())482 if isinstance(val, list):483 spec_file.append('{}: {}'.format(field, ' '.join(val)))484 elif val is not None:485 spec_file.append(f'{field}: {val}')486 487 if self.distribution.get_url():488 spec_file.append('Url: ' + self.distribution.get_url())489 490 if self.distribution_name:491 spec_file.append('Distribution: ' + self.distribution_name)492 493 if self.build_requires:494 spec_file.append('BuildRequires: ' + ' '.join(self.build_requires))495 496 if self.icon:497 spec_file.append('Icon: ' + os.path.basename(self.icon))498 499 if self.no_autoreq:500 spec_file.append('AutoReq: 0')501 502 spec_file.extend([503 '',504 '%description',505 self.distribution.get_long_description() or "",506 ])507 508 # put locale descriptions into spec file509 # XXX again, suppressed because config file syntax doesn't510 # easily support this ;-(511 # for locale in self.descriptions.keys():512 # spec_file.extend([513 # '',514 # '%description -l ' + locale,515 # self.descriptions[locale],516 # ])517 518 # rpm scripts519 # figure out default build script520 def_setup_call = f"{self.python} {os.path.basename(sys.argv[0])}"521 def_build = "%s build" % def_setup_call522 if self.use_rpm_opt_flags:523 def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build524 525 # insert contents of files526 527 # XXX this is kind of misleading: user-supplied options are files528 # that we open and interpolate into the spec file, but the defaults529 # are just text that we drop in as-is. Hmmm.530 531 install_cmd = (532 '%s install -O1 --root=$RPM_BUILD_ROOT ' '--record=INSTALLED_FILES'533 ) % def_setup_call534 535 script_options = [536 ('prep', 'prep_script', "%setup -n %{name}-%{unmangled_version}"),537 ('build', 'build_script', def_build),538 ('install', 'install_script', install_cmd),539 ('clean', 'clean_script', "rm -rf $RPM_BUILD_ROOT"),540 ('verifyscript', 'verify_script', None),541 ('pre', 'pre_install', None),542 ('post', 'post_install', None),543 ('preun', 'pre_uninstall', None),544 ('postun', 'post_uninstall', None),545 ]546 547 for rpm_opt, attr, default in script_options:548 # Insert contents of file referred to, if no file is referred to549 # use 'default' as contents of script550 val = getattr(self, attr)551 if val or default:552 spec_file.extend([553 '',554 '%' + rpm_opt,555 ])556 if val:557 with open(val) as f:558 spec_file.extend(f.read().split('\n'))559 else:560 spec_file.append(default)561 562 # files section563 spec_file.extend([564 '',565 '%files -f INSTALLED_FILES',566 '%defattr(-,root,root)',567 ])568 569 if self.doc_files:570 spec_file.append('%doc ' + ' '.join(self.doc_files))571 572 if self.changelog:573 spec_file.extend([574 '',575 '%changelog',576 ])577 spec_file.extend(self.changelog)578 579 return spec_file580 581 def _format_changelog(self, changelog):582 """Format the changelog correctly and convert it to a list of strings"""583 if not changelog:584 return changelog585 new_changelog = []586 for line in changelog.strip().split('\n'):587 line = line.strip()588 if line[0] == '*':589 new_changelog.extend(['', line])590 elif line[0] == '-':591 new_changelog.append(line)592 else:593 new_changelog.append(' ' + line)594 595 # strip trailing newline inserted by first changelog entry596 if not new_changelog[0]:597 del new_changelog[0]598 599 return new_changelog600 