1"""distutils.command.bdist_rpm 2 3Implements the Distutils 'bdist_rpm' command (create RPM source and binary 4distributions).""" 5 6__revision__ = "$Id$" 7 8import sys 9import os 10import string 11 12from distutils.core import Command 13from distutils.debug import DEBUG 14from distutils.file_util import write_file 15from distutils.sysconfig import get_python_version 16from distutils.errors import (DistutilsOptionError, DistutilsPlatformError, 17 DistutilsFileError, DistutilsExecError) 18from distutils import log 19 20class bdist_rpm (Command): 21 22 description = "create an RPM distribution" 23 24 user_options = [ 25 ('bdist-base=', None, 26 "base directory for creating built distributions"), 27 ('rpm-base=', None, 28 "base directory for creating RPMs (defaults to \"rpm\" under " 29 "--bdist-base; must be specified for RPM 2)"), 30 ('dist-dir=', 'd', 31 "directory to put final RPM files in " 32 "(and .spec files if --spec-only)"), 33 ('python=', None, 34 "path to Python interpreter to hard-code in the .spec file " 35 "(default: \"python\")"), 36 ('fix-python', None, 37 "hard-code the exact path to the current Python interpreter in " 38 "the .spec file"), 39 ('spec-only', None, 40 "only regenerate spec file"), 41 ('source-only', None, 42 "only generate source RPM"), 43 ('binary-only', None, 44 "only generate binary RPM"), 45 ('use-bzip2', None, 46 "use bzip2 instead of gzip to create source distribution"), 47 48 # More meta-data: too RPM-specific to put in the setup script, 49 # but needs to go in the .spec file -- so we make these options 50 # to "bdist_rpm". The idea is that packagers would put this 51 # info in setup.cfg, although they are of course free to 52 # supply it on the command line. 53 ('distribution-name=', None, 54 "name of the (Linux) distribution to which this " 55 "RPM applies (*not* the name of the module distribution!)"), 56 ('group=', None, 57 "package classification [default: \"Development/Libraries\"]"), 58 ('release=', None, 59 "RPM release number"), 60 ('serial=', None, 61 "RPM serial number"), 62 ('vendor=', None, 63 "RPM \"vendor\" (eg. \"Joe Blow <joe@example.com>\") " 64 "[default: maintainer or author from setup script]"), 65 ('packager=', None, 66 "RPM packager (eg. \"Jane Doe <jane@example.net>\")" 67 "[default: vendor]"), 68 ('doc-files=', None, 69 "list of documentation files (space or comma-separated)"), 70 ('changelog=', None, 71 "RPM changelog"), 72 ('icon=', None, 73 "name of icon file"), 74 ('provides=', None, 75 "capabilities provided by this package"), 76 ('requires=', None, 77 "capabilities required by this package"), 78 ('conflicts=', None, 79 "capabilities which conflict with this package"), 80 ('build-requires=', None, 81 "capabilities required to build this package"), 82 ('obsoletes=', None, 83 "capabilities made obsolete by this package"), 84 ('no-autoreq', None, 85 "do not automatically calculate dependencies"), 86 87 # Actions to take when building RPM 88 ('keep-temp', 'k', 89 "don't clean up RPM build directory"), 90 ('no-keep-temp', None, 91 "clean up RPM build directory [default]"), 92 ('use-rpm-opt-flags', None, 93 "compile with RPM_OPT_FLAGS when building from source RPM"), 94 ('no-rpm-opt-flags', None, 95 "do not pass any RPM CFLAGS to compiler"), 96 ('rpm3-mode', None, 97 "RPM 3 compatibility mode (default)"), 98 ('rpm2-mode', None, 99 "RPM 2 compatibility mode"), 100 101 # Add the hooks necessary for specifying custom scripts 102 ('prep-script=', None, 103 "Specify a script for the PREP phase of RPM building"), 104 ('build-script=', None, 105 "Specify a script for the BUILD phase of RPM building"), 106 107 ('pre-install=', None, 108 "Specify a script for the pre-INSTALL phase of RPM building"), 109 ('install-script=', None, 110 "Specify a script for the INSTALL phase of RPM building"), 111 ('post-install=', None, 112 "Specify a script for the post-INSTALL phase of RPM building"), 113 114 ('pre-uninstall=', None, 115 "Specify a script for the pre-UNINSTALL phase of RPM building"), 116 ('post-uninstall=', None, 117 "Specify a script for the post-UNINSTALL phase of RPM building"), 118 119 ('clean-script=', None, 120 "Specify a script for the CLEAN phase of RPM building"), 121 122 ('verify-script=', None, 123 "Specify a script for the VERIFY phase of the RPM build"), 124 125 # Allow a packager to explicitly force an architecture 126 ('force-arch=', None, 127 "Force an architecture onto the RPM build process"), 128 129 ('quiet', 'q', 130 "Run the INSTALL phase of RPM building in quiet mode"), 131 ] 132 133 boolean_options = ['keep-temp', 'use-rpm-opt-flags', 'rpm3-mode', 134 'no-autoreq', 'quiet'] 135 136 negative_opt = {'no-keep-temp': 'keep-temp', 137 'no-rpm-opt-flags': 'use-rpm-opt-flags', 138 'rpm2-mode': 'rpm3-mode'} 139 140 141 def initialize_options (self): 142 self.bdist_base = None 143 self.rpm_base = None 144 self.dist_dir = None 145 self.python = None 146 self.fix_python = None 147 self.spec_only = None 148 self.binary_only = None 149 self.source_only = None 150 self.use_bzip2 = None 151 152 self.distribution_name = None 153 self.group = None 154 self.release = None 155 self.serial = None 156 self.vendor = None 157 self.packager = None 158 self.doc_files = None 159 self.changelog = None 160 self.icon = None 161 162 self.prep_script = None 163 self.build_script = None 164 self.install_script = None 165 self.clean_script = None 166 self.verify_script = None 167 self.pre_install = None 168 self.post_install = None 169 self.pre_uninstall = None 170 self.post_uninstall = None 171 self.prep = None 172 self.provides = None 173 self.requires = None 174 self.conflicts = None 175 self.build_requires = None 176 self.obsoletes = None 177 178 self.keep_temp = 0 179 self.use_rpm_opt_flags = 1 180 self.rpm3_mode = 1 181 self.no_autoreq = 0 182 183 self.force_arch = None 184 self.quiet = 0 185 186 # initialize_options() 187 188 189 def finalize_options (self): 190 self.set_undefined_options('bdist', ('bdist_base', 'bdist_base')) 191 if self.rpm_base is None: 192 if not self.rpm3_mode: 193 raise DistutilsOptionError, \ 194 "you must specify --rpm-base in RPM 2 mode" 195 self.rpm_base = os.path.join(self.bdist_base, "rpm") 196 197 if self.python is None: 198 if self.fix_python: 199 self.python = sys.executable 200 else: 201 self.python = "python" 202 elif self.fix_python: 203 raise DistutilsOptionError, \ 204 "--python and --fix-python are mutually exclusive options" 205 206 if os.name != 'posix': 207 raise DistutilsPlatformError, \ 208 ("don't know how to create RPM " 209 "distributions on platform %s" % os.name) 210 if self.binary_only and self.source_only: 211 raise DistutilsOptionError, \ 212 "cannot supply both '--source-only' and '--binary-only'" 213 214 # don't pass CFLAGS to pure python distributions 215 if not self.distribution.has_ext_modules(): 216 self.use_rpm_opt_flags = 0 217 218 self.set_undefined_options('bdist', ('dist_dir', 'dist_dir')) 219 self.finalize_package_data() 220 221 # finalize_options() 222 223 def finalize_package_data (self): 224 self.ensure_string('group', "Development/Libraries") 225 self.ensure_string('vendor', 226 "%s <%s>" % (self.distribution.get_contact(), 227 self.distribution.get_contact_email())) 228 self.ensure_string('packager') 229 self.ensure_string_list('doc_files') 230 if isinstance(self.doc_files, list): 231 for readme in ('README', 'README.txt'): 232 if os.path.exists(readme) and readme not in self.doc_files: 233 self.doc_files.append(readme) 234 235 self.ensure_string('release', "1") 236 self.ensure_string('serial') # should it be an int? 237 238 self.ensure_string('distribution_name') 239 240 self.ensure_string('changelog') 241 # Format changelog correctly 242 self.changelog = self._format_changelog(self.changelog) 243 244 self.ensure_filename('icon') 245 246 self.ensure_filename('prep_script') 247 self.ensure_filename('build_script') 248 self.ensure_filename('install_script') 249 self.ensure_filename('clean_script') 250 self.ensure_filename('verify_script') 251 self.ensure_filename('pre_install') 252 self.ensure_filename('post_install') 253 self.ensure_filename('pre_uninstall') 254 self.ensure_filename('post_uninstall') 255 256 # XXX don't forget we punted on summaries and descriptions -- they 257 # should be handled here eventually! 258 259 # Now *this* is some meta-data that belongs in the setup script... 260 self.ensure_string_list('provides') 261 self.ensure_string_list('requires') 262 self.ensure_string_list('conflicts') 263 self.ensure_string_list('build_requires') 264 self.ensure_string_list('obsoletes') 265 266 self.ensure_string('force_arch') 267 # finalize_package_data () 268 269 270 def run (self): 271 272 if DEBUG: 273 print "before _get_package_data():" 274 print "vendor =", self.vendor 275 print "packager =", self.packager 276 print "doc_files =", self.doc_files 277 print "changelog =", self.changelog 278 279 # make directories 280 if self.spec_only: 281 spec_dir = self.dist_dir 282 self.mkpath(spec_dir) 283 else: 284 rpm_dir = {} 285 for d in ('SOURCES', 'SPECS', 'BUILD', 'RPMS', 'SRPMS'): 286 rpm_dir[d] = os.path.join(self.rpm_base, d) 287 self.mkpath(rpm_dir[d]) 288 spec_dir = rpm_dir['SPECS'] 289 290 # Spec file goes into 'dist_dir' if '--spec-only specified', 291 # build/rpm.<plat> otherwise. 292 spec_path = os.path.join(spec_dir, 293 "%s.spec" % self.distribution.get_name()) 294 self.execute(write_file, 295 (spec_path, 296 self._make_spec_file()), 297 "writing '%s'" % spec_path) 298 299 if self.spec_only: # stop if requested 300 return 301 302 # Make a source distribution and copy to SOURCES directory with 303 # optional icon. 304 saved_dist_files = self.distribution.dist_files[:] 305 sdist = self.reinitialize_command('sdist') 306 if self.use_bzip2: 307 sdist.formats = ['bztar'] 308 else: 309 sdist.formats = ['gztar'] 310 self.run_command('sdist') 311 self.distribution.dist_files = saved_dist_files 312 313 source = sdist.get_archive_files()[0] 314 source_dir = rpm_dir['SOURCES'] 315 self.copy_file(source, source_dir) 316 317 if self.icon: 318 if os.path.exists(self.icon): 319 self.copy_file(self.icon, source_dir) 320 else: 321 raise DistutilsFileError, \ 322 "icon file '%s' does not exist" % self.icon 323 324 325 # build package 326 log.info("building RPMs") 327 rpm_cmd = ['rpm'] 328 if os.path.exists('/usr/bin/rpmbuild') or \ 329 os.path.exists('/bin/rpmbuild'): 330 rpm_cmd = ['rpmbuild'] 331 332 if self.source_only: # what kind of RPMs? 333 rpm_cmd.append('-bs') 334 elif self.binary_only: 335 rpm_cmd.append('-bb') 336 else: 337 rpm_cmd.append('-ba') 338 if self.rpm3_mode: 339 rpm_cmd.extend(['--define', 340 '_topdir %s' % os.path.abspath(self.rpm_base)]) 341 if not self.keep_temp: 342 rpm_cmd.append('--clean') 343 344 if self.quiet: 345 rpm_cmd.append('--quiet') 346 347 rpm_cmd.append(spec_path) 348 # Determine the binary rpm names that should be built out of this spec 349 # file 350 # Note that some of these may not be really built (if the file 351 # list is empty) 352 nvr_string = "%{name}-%{version}-%{release}" 353 src_rpm = nvr_string + ".src.rpm" 354 non_src_rpm = "%{arch}/" + nvr_string + ".%{arch}.rpm" 355 q_cmd = r"rpm -q --qf '%s %s\n' --specfile '%s'" % ( 356 src_rpm, non_src_rpm, spec_path) 357 358 out = os.popen(q_cmd) 359 try: 360 binary_rpms = [] 361 source_rpm = None 362 while 1: 363 line = out.readline() 364 if not line: 365 break 366 l = string.split(string.strip(line)) 367 assert(len(l) == 2) 368 binary_rpms.append(l[1]) 369 # The source rpm is named after the first entry in the spec file 370 if source_rpm is None: 371 source_rpm = l[0] 372 373 status = out.close() 374 if status: 375 raise DistutilsExecError("Failed to execute: %s" % repr(q_cmd)) 376 377 finally: 378 out.close() 379 380 self.spawn(rpm_cmd) 381 382 if not self.dry_run: 383 if self.distribution.has_ext_modules(): 384 pyversion = get_python_version() 385 else: 386 pyversion = 'any' 387 388 if not self.binary_only: 389 srpm = os.path.join(rpm_dir['SRPMS'], source_rpm) 390 assert(os.path.exists(srpm)) 391 self.move_file(srpm, self.dist_dir) 392 filename = os.path.join(self.dist_dir, source_rpm) 393 self.distribution.dist_files.append( 394 ('bdist_rpm', pyversion, filename)) 395 396 if not self.source_only: 397 for rpm in binary_rpms: 398 rpm = os.path.join(rpm_dir['RPMS'], rpm) 399 if os.path.exists(rpm): 400 self.move_file(rpm, self.dist_dir) 401 filename = os.path.join(self.dist_dir, 402 os.path.basename(rpm)) 403 self.distribution.dist_files.append( 404 ('bdist_rpm', pyversion, filename)) 405 # run() 406 407 def _dist_path(self, path): 408 return os.path.join(self.dist_dir, os.path.basename(path)) 409 410 def _make_spec_file(self): 411 """Generate the text of an RPM spec file and return it as a 412 list of strings (one per line). 413 """ 414 # definitions and headers 415 spec_file = [ 416 '%define name ' + self.distribution.get_name(), 417 '%define version ' + self.distribution.get_version().replace('-','_'), 418 '%define unmangled_version ' + self.distribution.get_version(), 419 '%define release ' + self.release.replace('-','_'), 420 '', 421 'Summary: ' + self.distribution.get_description(), 422 ] 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, string.lower(field)) 465 if isinstance(val, list): 466 spec_file.append('%s: %s' % (field, string.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 string.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(string.split(open(val, 'r').read(), '\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 ' + string.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 # _make_spec_file () 564 565 def _format_changelog(self, changelog): 566 """Format the changelog correctly and convert it to a list of strings 567 """ 568 if not changelog: 569 return changelog 570 new_changelog = [] 571 for line in string.split(string.strip(changelog), '\n'): 572 line = string.strip(line) 573 if line[0] == '*': 574 new_changelog.extend(['', line]) 575 elif line[0] == '-': 576 new_changelog.append(line) 577 else: 578 new_changelog.append(' ' + line) 579 580 # strip trailing newline inserted by first changelog entry 581 if not new_changelog[0]: 582 del new_changelog[0] 583 584 return new_changelog 585 586 # _format_changelog() 587 588# class bdist_rpm 589