| 1 | """distutils.command.bdist_rpm |
| 2 | |
| 3 | Implements the Distutils 'bdist_rpm' command (create RPM source and binary |
| 4 | distributions).""" |
| 5 | |
| 6 | # This module should be kept compatible with Python 2.1. |
| 7 | |
| 8 | __revision__ = "$Id: bdist_rpm.py,v 1.46 2004/11/10 22:23:14 loewis Exp $" |
| 9 | |
| 10 | import sys, os, string |
| 11 | import glob |
| 12 | from types import * |
| 13 | from distutils.core import Command |
| 14 | from distutils.debug import DEBUG |
| 15 | from distutils.util import get_platform |
| 16 | from distutils.file_util import write_file |
| 17 | from distutils.errors import * |
| 18 | from distutils import log |
| 19 | |
| 20 | class 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 | |
| 130 | boolean_options = ['keep-temp', 'use-rpm-opt-flags', 'rpm3-mode', |
| 131 | 'no-autoreq'] |
| 132 | |
| 133 | negative_opt = {'no-keep-temp': 'keep-temp', |
| 134 | 'no-rpm-opt-flags': 'use-rpm-opt-flags', |
| 135 | 'rpm2-mode': 'rpm3-mode'} |
| 136 | |
| 137 | |
| 138 | def initialize_options (self): |
| 139 | self.bdist_base = None |
| 140 | self.rpm_base = None |
| 141 | self.dist_dir = None |
| 142 | self.python = None |
| 143 | self.fix_python = None |
| 144 | self.spec_only = None |
| 145 | self.binary_only = None |
| 146 | self.source_only = None |
| 147 | self.use_bzip2 = None |
| 148 | |
| 149 | self.distribution_name = None |
| 150 | self.group = None |
| 151 | self.release = None |
| 152 | self.serial = None |
| 153 | self.vendor = None |
| 154 | self.packager = None |
| 155 | self.doc_files = None |
| 156 | self.changelog = None |
| 157 | self.icon = None |
| 158 | |
| 159 | self.prep_script = None |
| 160 | self.build_script = None |
| 161 | self.install_script = None |
| 162 | self.clean_script = None |
| 163 | self.verify_script = None |
| 164 | self.pre_install = None |
| 165 | self.post_install = None |
| 166 | self.pre_uninstall = None |
| 167 | self.post_uninstall = None |
| 168 | self.prep = None |
| 169 | self.provides = None |
| 170 | self.requires = None |
| 171 | self.conflicts = None |
| 172 | self.build_requires = None |
| 173 | self.obsoletes = None |
| 174 | |
| 175 | self.keep_temp = 0 |
| 176 | self.use_rpm_opt_flags = 1 |
| 177 | self.rpm3_mode = 1 |
| 178 | self.no_autoreq = 0 |
| 179 | |
| 180 | self.force_arch = None |
| 181 | |
| 182 | # initialize_options() |
| 183 | |
| 184 | |
| 185 | def finalize_options (self): |
| 186 | self.set_undefined_options('bdist', ('bdist_base', 'bdist_base')) |
| 187 | if self.rpm_base is None: |
| 188 | if not self.rpm3_mode: |
| 189 | raise DistutilsOptionError, \ |
| 190 | "you must specify --rpm-base in RPM 2 mode" |
| 191 | self.rpm_base = os.path.join(self.bdist_base, "rpm") |
| 192 | |
| 193 | if self.python is None: |
| 194 | if self.fix_python: |
| 195 | self.python = sys.executable |
| 196 | else: |
| 197 | self.python = "python" |
| 198 | elif self.fix_python: |
| 199 | raise DistutilsOptionError, \ |
| 200 | "--python and --fix-python are mutually exclusive options" |
| 201 | |
| 202 | if os.name != 'posix': |
| 203 | raise DistutilsPlatformError, \ |
| 204 | ("don't know how to create RPM " |
| 205 | "distributions on platform %s" % os.name) |
| 206 | if self.binary_only and self.source_only: |
| 207 | raise DistutilsOptionError, \ |
| 208 | "cannot supply both '--source-only' and '--binary-only'" |
| 209 | |
| 210 | # don't pass CFLAGS to pure python distributions |
| 211 | if not self.distribution.has_ext_modules(): |
| 212 | self.use_rpm_opt_flags = 0 |
| 213 | |
| 214 | self.set_undefined_options('bdist', ('dist_dir', 'dist_dir')) |
| 215 | self.finalize_package_data() |
| 216 | |
| 217 | # finalize_options() |
| 218 | |
| 219 | def finalize_package_data (self): |
| 220 | self.ensure_string('group', "Development/Libraries") |
| 221 | self.ensure_string('vendor', |
| 222 | "%s <%s>" % (self.distribution.get_contact(), |
| 223 | self.distribution.get_contact_email())) |
| 224 | self.ensure_string('packager') |
| 225 | self.ensure_string_list('doc_files') |
| 226 | if type(self.doc_files) is ListType: |
| 227 | for readme in ('README', 'README.txt'): |
| 228 | if os.path.exists(readme) and readme not in self.doc_files: |
| 229 | self.doc_files.append(readme) |
| 230 | |
| 231 | self.ensure_string('release', "1") |
| 232 | self.ensure_string('serial') # should it be an int? |
| 233 | |
| 234 | self.ensure_string('distribution_name') |
| 235 | |
| 236 | self.ensure_string('changelog') |
| 237 | # Format changelog correctly |
| 238 | self.changelog = self._format_changelog(self.changelog) |
| 239 | |
| 240 | self.ensure_filename('icon') |
| 241 | |
| 242 | self.ensure_filename('prep_script') |
| 243 | self.ensure_filename('build_script') |
| 244 | self.ensure_filename('install_script') |
| 245 | self.ensure_filename('clean_script') |
| 246 | self.ensure_filename('verify_script') |
| 247 | self.ensure_filename('pre_install') |
| 248 | self.ensure_filename('post_install') |
| 249 | self.ensure_filename('pre_uninstall') |
| 250 | self.ensure_filename('post_uninstall') |
| 251 | |
| 252 | # XXX don't forget we punted on summaries and descriptions -- they |
| 253 | # should be handled here eventually! |
| 254 | |
| 255 | # Now *this* is some meta-data that belongs in the setup script... |
| 256 | self.ensure_string_list('provides') |
| 257 | self.ensure_string_list('requires') |
| 258 | self.ensure_string_list('conflicts') |
| 259 | self.ensure_string_list('build_requires') |
| 260 | self.ensure_string_list('obsoletes') |
| 261 | |
| 262 | self.ensure_string('force_arch') |
| 263 | # finalize_package_data () |
| 264 | |
| 265 | |
| 266 | def run (self): |
| 267 | |
| 268 | if DEBUG: |
| 269 | print "before _get_package_data():" |
| 270 | print "vendor =", self.vendor |
| 271 | print "packager =", self.packager |
| 272 | print "doc_files =", self.doc_files |
| 273 | print "changelog =", self.changelog |
| 274 | |
| 275 | # make directories |
| 276 | if self.spec_only: |
| 277 | spec_dir = self.dist_dir |
| 278 | self.mkpath(spec_dir) |
| 279 | else: |
| 280 | rpm_dir = {} |
| 281 | for d in ('SOURCES', 'SPECS', 'BUILD', 'RPMS', 'SRPMS'): |
| 282 | rpm_dir[d] = os.path.join(self.rpm_base, d) |
| 283 | self.mkpath(rpm_dir[d]) |
| 284 | spec_dir = rpm_dir['SPECS'] |
| 285 | |
| 286 | # Spec file goes into 'dist_dir' if '--spec-only specified', |
| 287 | # build/rpm.<plat> otherwise. |
| 288 | spec_path = os.path.join(spec_dir, |
| 289 | "%s.spec" % self.distribution.get_name()) |
| 290 | self.execute(write_file, |
| 291 | (spec_path, |
| 292 | self._make_spec_file()), |
| 293 | "writing '%s'" % spec_path) |
| 294 | |
| 295 | if self.spec_only: # stop if requested |
| 296 | return |
| 297 | |
| 298 | # Make a source distribution and copy to SOURCES directory with |
| 299 | # optional icon. |
| 300 | sdist = self.reinitialize_command('sdist') |
| 301 | if self.use_bzip2: |
| 302 | sdist.formats = ['bztar'] |
| 303 | else: |
| 304 | sdist.formats = ['gztar'] |
| 305 | self.run_command('sdist') |
| 306 | |
| 307 | source = sdist.get_archive_files()[0] |
| 308 | source_dir = rpm_dir['SOURCES'] |
| 309 | self.copy_file(source, source_dir) |
| 310 | |
| 311 | if self.icon: |
| 312 | if os.path.exists(self.icon): |
| 313 | self.copy_file(self.icon, source_dir) |
| 314 | else: |
| 315 | raise DistutilsFileError, \ |
| 316 | "icon file '%s' does not exist" % self.icon |
| 317 | |
| 318 | |
| 319 | # build package |
| 320 | log.info("building RPMs") |
| 321 | rpm_cmd = ['rpm'] |
| 322 | if os.path.exists('/usr/bin/rpmbuild') or \ |
| 323 | os.path.exists('/bin/rpmbuild'): |
| 324 | rpm_cmd = ['rpmbuild'] |
| 325 | if self.source_only: # what kind of RPMs? |
| 326 | rpm_cmd.append('-bs') |
| 327 | elif self.binary_only: |
| 328 | rpm_cmd.append('-bb') |
| 329 | else: |
| 330 | rpm_cmd.append('-ba') |
| 331 | if self.rpm3_mode: |
| 332 | rpm_cmd.extend(['--define', |
| 333 | '_topdir %s' % os.path.abspath(self.rpm_base)]) |
| 334 | if not self.keep_temp: |
| 335 | rpm_cmd.append('--clean') |
| 336 | rpm_cmd.append(spec_path) |
| 337 | self.spawn(rpm_cmd) |
| 338 | |
| 339 | # XXX this is a nasty hack -- we really should have a proper way to |
| 340 | # find out the names of the RPM files created; also, this assumes |
| 341 | # that RPM creates exactly one source and one binary RPM. |
| 342 | if not self.dry_run: |
| 343 | if not self.binary_only: |
| 344 | srpms = glob.glob(os.path.join(rpm_dir['SRPMS'], "*.rpm")) |
| 345 | assert len(srpms) == 1, \ |
| 346 | "unexpected number of SRPM files found: %s" % srpms |
| 347 | self.move_file(srpms[0], self.dist_dir) |
| 348 | |
| 349 | if not self.source_only: |
| 350 | rpms = glob.glob(os.path.join(rpm_dir['RPMS'], "*/*.rpm")) |
| 351 | debuginfo = glob.glob(os.path.join(rpm_dir['RPMS'], \ |
| 352 | "*/*debuginfo*.rpm")) |
| 353 | if debuginfo: |
| 354 | rpms.remove(debuginfo[0]) |
| 355 | assert len(rpms) == 1, \ |
| 356 | "unexpected number of RPM files found: %s" % rpms |
| 357 | self.move_file(rpms[0], self.dist_dir) |
| 358 | if debuginfo: |
| 359 | self.move_file(debuginfo[0], self.dist_dir) |
| 360 | # run() |
| 361 | |
| 362 | |
| 363 | def _make_spec_file(self): |
| 364 | """Generate the text of an RPM spec file and return it as a |
| 365 | list of strings (one per line). |
| 366 | """ |
| 367 | # definitions and headers |
| 368 | spec_file = [ |
| 369 | '%define name ' + self.distribution.get_name(), |
| 370 | '%define version ' + self.distribution.get_version().replace('-','_'), |
| 371 | '%define release ' + self.release.replace('-','_'), |
| 372 | '', |
| 373 | 'Summary: ' + self.distribution.get_description(), |
| 374 | ] |
| 375 | |
| 376 | # put locale summaries into spec file |
| 377 | # XXX not supported for now (hard to put a dictionary |
| 378 | # in a config file -- arg!) |
| 379 | #for locale in self.summaries.keys(): |
| 380 | # spec_file.append('Summary(%s): %s' % (locale, |
| 381 | # self.summaries[locale])) |
| 382 | |
| 383 | spec_file.extend([ |
| 384 | 'Name: %{name}', |
| 385 | 'Version: %{version}', |
| 386 | 'Release: %{release}',]) |
| 387 | |
| 388 | # XXX yuck! this filename is available from the "sdist" command, |
| 389 | # but only after it has run: and we create the spec file before |
| 390 | # running "sdist", in case of --spec-only. |
| 391 | if self.use_bzip2: |
| 392 | spec_file.append('Source0: %{name}-%{version}.tar.bz2') |
| 393 | else: |
| 394 | spec_file.append('Source0: %{name}-%{version}.tar.gz') |
| 395 | |
| 396 | spec_file.extend([ |
| 397 | 'License: ' + self.distribution.get_license(), |
| 398 | 'Group: ' + self.group, |
| 399 | 'BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot', |
| 400 | 'Prefix: %{_prefix}', ]) |
| 401 | |
| 402 | if not self.force_arch: |
| 403 | # noarch if no extension modules |
| 404 | if not self.distribution.has_ext_modules(): |
| 405 | spec_file.append('BuildArch: noarch') |
| 406 | else: |
| 407 | spec_file.append( 'BuildArch: %s' % self.force_arch ) |
| 408 | |
| 409 | for field in ('Vendor', |
| 410 | 'Packager', |
| 411 | 'Provides', |
| 412 | 'Requires', |
| 413 | 'Conflicts', |
| 414 | 'Obsoletes', |
| 415 | ): |
| 416 | val = getattr(self, string.lower(field)) |
| 417 | if type(val) is ListType: |
| 418 | spec_file.append('%s: %s' % (field, string.join(val))) |
| 419 | elif val is not None: |
| 420 | spec_file.append('%s: %s' % (field, val)) |
| 421 | |
| 422 | |
| 423 | if self.distribution.get_url() != 'UNKNOWN': |
| 424 | spec_file.append('Url: ' + self.distribution.get_url()) |
| 425 | |
| 426 | if self.distribution_name: |
| 427 | spec_file.append('Distribution: ' + self.distribution_name) |
| 428 | |
| 429 | if self.build_requires: |
| 430 | spec_file.append('BuildRequires: ' + |
| 431 | string.join(self.build_requires)) |
| 432 | |
| 433 | if self.icon: |
| 434 | spec_file.append('Icon: ' + os.path.basename(self.icon)) |
| 435 | |
| 436 | if self.no_autoreq: |
| 437 | spec_file.append('AutoReq: 0') |
| 438 | |
| 439 | spec_file.extend([ |
| 440 | '', |
| 441 | '%description', |
| 442 | self.distribution.get_long_description() |
| 443 | ]) |
| 444 | |
| 445 | # put locale descriptions into spec file |
| 446 | # XXX again, suppressed because config file syntax doesn't |
| 447 | # easily support this ;-( |
| 448 | #for locale in self.descriptions.keys(): |
| 449 | # spec_file.extend([ |
| 450 | # '', |
| 451 | # '%description -l ' + locale, |
| 452 | # self.descriptions[locale], |
| 453 | # ]) |
| 454 | |
| 455 | # rpm scripts |
| 456 | # figure out default build script |
| 457 | def_build = "%s setup.py build" % self.python |
| 458 | if self.use_rpm_opt_flags: |
| 459 | def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build |
| 460 | |
| 461 | # insert contents of files |
| 462 | |
| 463 | # XXX this is kind of misleading: user-supplied options are files |
| 464 | # that we open and interpolate into the spec file, but the defaults |
| 465 | # are just text that we drop in as-is. Hmmm. |
| 466 | |
| 467 | script_options = [ |
| 468 | ('prep', 'prep_script', "%setup"), |
| 469 | ('build', 'build_script', def_build), |
| 470 | ('install', 'install_script', |
| 471 | ("%s setup.py install " |
| 472 | "--root=$RPM_BUILD_ROOT " |
| 473 | "--record=INSTALLED_FILES") % self.python), |
| 474 | ('clean', 'clean_script', "rm -rf $RPM_BUILD_ROOT"), |
| 475 | ('verifyscript', 'verify_script', None), |
| 476 | ('pre', 'pre_install', None), |
| 477 | ('post', 'post_install', None), |
| 478 | ('preun', 'pre_uninstall', None), |
| 479 | ('postun', 'post_uninstall', None), |
| 480 | ] |
| 481 | |
| 482 | for (rpm_opt, attr, default) in script_options: |
| 483 | # Insert contents of file referred to, if no file is referred to |
| 484 | # use 'default' as contents of script |
| 485 | val = getattr(self, attr) |
| 486 | if val or default: |
| 487 | spec_file.extend([ |
| 488 | '', |
| 489 | '%' + rpm_opt,]) |
| 490 | if val: |
| 491 | spec_file.extend(string.split(open(val, 'r').read(), '\n')) |
| 492 | else: |
| 493 | spec_file.append(default) |
| 494 | |
| 495 | |
| 496 | # files section |
| 497 | spec_file.extend([ |
| 498 | '', |
| 499 | '%files -f INSTALLED_FILES', |
| 500 | '%defattr(-,root,root)', |
| 501 | ]) |
| 502 | |
| 503 | if self.doc_files: |
| 504 | spec_file.append('%doc ' + string.join(self.doc_files)) |
| 505 | |
| 506 | if self.changelog: |
| 507 | spec_file.extend([ |
| 508 | '', |
| 509 | '%changelog',]) |
| 510 | spec_file.extend(self.changelog) |
| 511 | |
| 512 | return spec_file |
| 513 | |
| 514 | # _make_spec_file () |
| 515 | |
| 516 | def _format_changelog(self, changelog): |
| 517 | """Format the changelog correctly and convert it to a list of strings |
| 518 | """ |
| 519 | if not changelog: |
| 520 | return changelog |
| 521 | new_changelog = [] |
| 522 | for line in string.split(string.strip(changelog), '\n'): |
| 523 | line = string.strip(line) |
| 524 | if line[0] == '*': |
| 525 | new_changelog.extend(['', line]) |
| 526 | elif line[0] == '-': |
| 527 | new_changelog.append(line) |
| 528 | else: |
| 529 | new_changelog.append(' ' + line) |
| 530 | |
| 531 | # strip trailing newline inserted by first changelog entry |
| 532 | if not new_changelog[0]: |
| 533 | del new_changelog[0] |
| 534 | |
| 535 | return new_changelog |
| 536 | |
| 537 | # _format_changelog() |
| 538 | |
| 539 | # class bdist_rpm |