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