Commit | Line | Data |
---|---|---|
920dae64 AT |
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 |