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