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