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