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 = ['rpmbuild'] 313 314 if self.source_only: # what kind of RPMs? 315 rpm_cmd.append('-bs') 316 elif self.binary_only: 317 rpm_cmd.append('-bb') 318 else: 319 rpm_cmd.append('-ba') 320 rpm_cmd.extend(['--define', '__python %s' % self.python]) 321 if self.rpm3_mode: 322 rpm_cmd.extend(['--define', 323 '_topdir %s' % os.path.abspath(self.rpm_base)]) 324 if not self.keep_temp: 325 rpm_cmd.append('--clean') 326 327 if self.quiet: 328 rpm_cmd.append('--quiet') 329 330 rpm_cmd.append(spec_path) 331 # Determine the binary rpm names that should be built out of this spec 332 # file 333 # Note that some of these may not be really built (if the file 334 # list is empty) 335 nvr_string = "%{name}-%{version}-%{release}" 336 src_rpm = nvr_string + ".src.rpm" 337 non_src_rpm = "%{arch}/" + nvr_string + ".%{arch}.rpm" 338 q_cmd = r"rpm -q --qf '%s %s\n' --specfile '%s'" % ( 339 src_rpm, non_src_rpm, spec_path) 340 341 out = os.popen(q_cmd) 342 try: 343 binary_rpms = [] 344 source_rpm = None 345 while True: 346 line = out.readline() 347 if not line: 348 break 349 l = line.strip().split() 350 assert(len(l) == 2) 351 binary_rpms.append(l[1]) 352 # The source rpm is named after the first entry in the spec file 353 if source_rpm is None: 354 source_rpm = l[0] 355 356 status = out.close() 357 if status: 358 raise DistutilsExecError("Failed to execute: %s" % repr(q_cmd)) 359 360 finally: 361 out.close() 362 363 self.spawn(rpm_cmd) 364 365 if not self.dry_run: 366 if self.distribution.has_ext_modules(): 367 pyversion = get_python_version() 368 else: 369 pyversion = 'any' 370 371 if not self.binary_only: 372 srpm = os.path.join(rpm_dir['SRPMS'], source_rpm) 373 assert(os.path.exists(srpm)) 374 self.move_file(srpm, self.dist_dir) 375 filename = os.path.join(self.dist_dir, source_rpm) 376 self.distribution.dist_files.append( 377 ('bdist_rpm', pyversion, filename)) 378 379 if not self.source_only: 380 for rpm in binary_rpms: 381 rpm = os.path.join(rpm_dir['RPMS'], rpm) 382 if os.path.exists(rpm): 383 self.move_file(rpm, self.dist_dir) 384 filename = os.path.join(self.dist_dir, 385 os.path.basename(rpm)) 386 self.distribution.dist_files.append( 387 ('bdist_rpm', pyversion, filename)) 388 389 def _dist_path(self, path): 390 return os.path.join(self.dist_dir, os.path.basename(path)) 391 392 def _make_spec_file(self): 393 """Generate the text of an RPM spec file and return it as a 394 list of strings (one per line). 395 """ 396 # definitions and headers 397 spec_file = [ 398 '%define name ' + self.distribution.get_name(), 399 '%define version ' + self.distribution.get_version().replace('-','_'), 400 '%define unmangled_version ' + self.distribution.get_version(), 401 '%define release ' + self.release.replace('-','_'), 402 '', 403 'Summary: ' + self.distribution.get_description(), 404 ] 405 406 # Workaround for #14443 which affects some RPM based systems such as 407 # RHEL6 (and probably derivatives) 408 vendor_hook = subprocess.getoutput('rpm --eval %{__os_install_post}') 409 # Generate a potential replacement value for __os_install_post (whilst 410 # normalizing the whitespace to simplify the test for whether the 411 # invocation of brp-python-bytecompile passes in __python): 412 vendor_hook = '\n'.join([' %s \\' % line.strip() 413 for line in vendor_hook.splitlines()]) 414 problem = "brp-python-bytecompile \\\n" 415 fixed = "brp-python-bytecompile %{__python} \\\n" 416 fixed_hook = vendor_hook.replace(problem, fixed) 417 if fixed_hook != vendor_hook: 418 spec_file.append('# Workaround for http://bugs.python.org/issue14443') 419 spec_file.append('%define __os_install_post ' + fixed_hook + '\n') 420 421 # put locale summaries into spec file 422 # XXX not supported for now (hard to put a dictionary 423 # in a config file -- arg!) 424 #for locale in self.summaries.keys(): 425 # spec_file.append('Summary(%s): %s' % (locale, 426 # self.summaries[locale])) 427 428 spec_file.extend([ 429 'Name: %{name}', 430 'Version: %{version}', 431 'Release: %{release}',]) 432 433 # XXX yuck! this filename is available from the "sdist" command, 434 # but only after it has run: and we create the spec file before 435 # running "sdist", in case of --spec-only. 436 if self.use_bzip2: 437 spec_file.append('Source0: %{name}-%{unmangled_version}.tar.bz2') 438 else: 439 spec_file.append('Source0: %{name}-%{unmangled_version}.tar.gz') 440 441 spec_file.extend([ 442 'License: ' + self.distribution.get_license(), 443 'Group: ' + self.group, 444 'BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot', 445 'Prefix: %{_prefix}', ]) 446 447 if not self.force_arch: 448 # noarch if no extension modules 449 if not self.distribution.has_ext_modules(): 450 spec_file.append('BuildArch: noarch') 451 else: 452 spec_file.append( 'BuildArch: %s' % self.force_arch ) 453 454 for field in ('Vendor', 455 'Packager', 456 'Provides', 457 'Requires', 458 'Conflicts', 459 'Obsoletes', 460 ): 461 val = getattr(self, field.lower()) 462 if isinstance(val, list): 463 spec_file.append('%s: %s' % (field, ' '.join(val))) 464 elif val is not None: 465 spec_file.append('%s: %s' % (field, val)) 466 467 468 if self.distribution.get_url() != 'UNKNOWN': 469 spec_file.append('Url: ' + self.distribution.get_url()) 470 471 if self.distribution_name: 472 spec_file.append('Distribution: ' + self.distribution_name) 473 474 if self.build_requires: 475 spec_file.append('BuildRequires: ' + 476 ' '.join(self.build_requires)) 477 478 if self.icon: 479 spec_file.append('Icon: ' + os.path.basename(self.icon)) 480 481 if self.no_autoreq: 482 spec_file.append('AutoReq: 0') 483 484 spec_file.extend([ 485 '', 486 '%description', 487 self.distribution.get_long_description() 488 ]) 489 490 # put locale descriptions into spec file 491 # XXX again, suppressed because config file syntax doesn't 492 # easily support this ;-( 493 #for locale in self.descriptions.keys(): 494 # spec_file.extend([ 495 # '', 496 # '%description -l ' + locale, 497 # self.descriptions[locale], 498 # ]) 499 500 # rpm scripts 501 # figure out default build script 502 def_setup_call = "%s %s" % (self.python,os.path.basename(sys.argv[0])) 503 def_build = "%s build" % def_setup_call 504 if self.use_rpm_opt_flags: 505 def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build 506 507 # insert contents of files 508 509 # XXX this is kind of misleading: user-supplied options are files 510 # that we open and interpolate into the spec file, but the defaults 511 # are just text that we drop in as-is. Hmmm. 512 513 install_cmd = ('%s install -O1 --root=$RPM_BUILD_ROOT ' 514 '--record=INSTALLED_FILES') % def_setup_call 515 516 script_options = [ 517 ('prep', 'prep_script', "%setup -n %{name}-%{unmangled_version}"), 518 ('build', 'build_script', def_build), 519 ('install', 'install_script', install_cmd), 520 ('clean', 'clean_script', "rm -rf $RPM_BUILD_ROOT"), 521 ('verifyscript', 'verify_script', None), 522 ('pre', 'pre_install', None), 523 ('post', 'post_install', None), 524 ('preun', 'pre_uninstall', None), 525 ('postun', 'post_uninstall', None), 526 ] 527 528 for (rpm_opt, attr, default) in script_options: 529 # Insert contents of file referred to, if no file is referred to 530 # use 'default' as contents of script 531 val = getattr(self, attr) 532 if val or default: 533 spec_file.extend([ 534 '', 535 '%' + rpm_opt,]) 536 if val: 537 with open(val) as f: 538 spec_file.extend(f.read().split('\n')) 539 else: 540 spec_file.append(default) 541 542 543 # files section 544 spec_file.extend([ 545 '', 546 '%files -f INSTALLED_FILES', 547 '%defattr(-,root,root)', 548 ]) 549 550 if self.doc_files: 551 spec_file.append('%doc ' + ' '.join(self.doc_files)) 552 553 if self.changelog: 554 spec_file.extend([ 555 '', 556 '%changelog',]) 557 spec_file.extend(self.changelog) 558 559 return spec_file 560 561 def _format_changelog(self, changelog): 562 """Format the changelog correctly and convert it to a list of strings 563 """ 564 if not changelog: 565 return changelog 566 new_changelog = [] 567 for line in changelog.strip().split('\n'): 568 line = line.strip() 569 if line[0] == '*': 570 new_changelog.extend(['', line]) 571 elif line[0] == '-': 572 new_changelog.append(line) 573 else: 574 new_changelog.append(' ' + line) 575 576 # strip trailing newline inserted by first changelog entry 577 if not new_changelog[0]: 578 del new_changelog[0] 579 580 return new_changelog 581