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.file_util import write_file 10from distutils.errors import * 11from distutils.sysconfig import get_python_version 12from distutils import log 13 14class bdist_rpm(Command): 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 580