bdist_rpm.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. """distutils.command.bdist_rpm
  2. Implements the Distutils 'bdist_rpm' command (create RPM source and binary
  3. distributions)."""
  4. import os
  5. import subprocess
  6. import sys
  7. from distutils._log import log
  8. from typing import ClassVar
  9. from ..core import Command
  10. from ..debug import DEBUG
  11. from ..errors import (
  12. DistutilsExecError,
  13. DistutilsFileError,
  14. DistutilsOptionError,
  15. DistutilsPlatformError,
  16. )
  17. from ..file_util import write_file
  18. from ..sysconfig import get_python_version
  19. class bdist_rpm(Command):
  20. description = "create an RPM distribution"
  21. user_options = [
  22. ('bdist-base=', None, "base directory for creating built distributions"),
  23. (
  24. 'rpm-base=',
  25. None,
  26. "base directory for creating RPMs (defaults to \"rpm\" under "
  27. "--bdist-base; must be specified for RPM 2)",
  28. ),
  29. (
  30. 'dist-dir=',
  31. 'd',
  32. "directory to put final RPM files in (and .spec files if --spec-only)",
  33. ),
  34. (
  35. 'python=',
  36. None,
  37. "path to Python interpreter to hard-code in the .spec file "
  38. "[default: \"python\"]",
  39. ),
  40. (
  41. 'fix-python',
  42. None,
  43. "hard-code the exact path to the current Python interpreter in "
  44. "the .spec file",
  45. ),
  46. ('spec-only', None, "only regenerate spec file"),
  47. ('source-only', None, "only generate source RPM"),
  48. ('binary-only', None, "only generate binary RPM"),
  49. ('use-bzip2', None, "use bzip2 instead of gzip to create source distribution"),
  50. # More meta-data: too RPM-specific to put in the setup script,
  51. # but needs to go in the .spec file -- so we make these options
  52. # to "bdist_rpm". The idea is that packagers would put this
  53. # info in setup.cfg, although they are of course free to
  54. # supply it on the command line.
  55. (
  56. 'distribution-name=',
  57. None,
  58. "name of the (Linux) distribution to which this "
  59. "RPM applies (*not* the name of the module distribution!)",
  60. ),
  61. ('group=', None, "package classification [default: \"Development/Libraries\"]"),
  62. ('release=', None, "RPM release number"),
  63. ('serial=', None, "RPM serial number"),
  64. (
  65. 'vendor=',
  66. None,
  67. "RPM \"vendor\" (eg. \"Joe Blow <joe@example.com>\") "
  68. "[default: maintainer or author from setup script]",
  69. ),
  70. (
  71. 'packager=',
  72. None,
  73. "RPM packager (eg. \"Jane Doe <jane@example.net>\") [default: vendor]",
  74. ),
  75. ('doc-files=', None, "list of documentation files (space or comma-separated)"),
  76. ('changelog=', None, "RPM changelog"),
  77. ('icon=', None, "name of icon file"),
  78. ('provides=', None, "capabilities provided by this package"),
  79. ('requires=', None, "capabilities required by this package"),
  80. ('conflicts=', None, "capabilities which conflict with this package"),
  81. ('build-requires=', None, "capabilities required to build this package"),
  82. ('obsoletes=', None, "capabilities made obsolete by this package"),
  83. ('no-autoreq', None, "do not automatically calculate dependencies"),
  84. # Actions to take when building RPM
  85. ('keep-temp', 'k', "don't clean up RPM build directory"),
  86. ('no-keep-temp', None, "clean up RPM build directory [default]"),
  87. (
  88. 'use-rpm-opt-flags',
  89. None,
  90. "compile with RPM_OPT_FLAGS when building from source RPM",
  91. ),
  92. ('no-rpm-opt-flags', None, "do not pass any RPM CFLAGS to compiler"),
  93. ('rpm3-mode', None, "RPM 3 compatibility mode (default)"),
  94. ('rpm2-mode', None, "RPM 2 compatibility mode"),
  95. # Add the hooks necessary for specifying custom scripts
  96. ('prep-script=', None, "Specify a script for the PREP phase of RPM building"),
  97. ('build-script=', None, "Specify a script for the BUILD phase of RPM building"),
  98. (
  99. 'pre-install=',
  100. None,
  101. "Specify a script for the pre-INSTALL phase of RPM building",
  102. ),
  103. (
  104. 'install-script=',
  105. None,
  106. "Specify a script for the INSTALL phase of RPM building",
  107. ),
  108. (
  109. 'post-install=',
  110. None,
  111. "Specify a script for the post-INSTALL phase of RPM building",
  112. ),
  113. (
  114. 'pre-uninstall=',
  115. None,
  116. "Specify a script for the pre-UNINSTALL phase of RPM building",
  117. ),
  118. (
  119. 'post-uninstall=',
  120. None,
  121. "Specify a script for the post-UNINSTALL phase of RPM building",
  122. ),
  123. ('clean-script=', None, "Specify a script for the CLEAN phase of RPM building"),
  124. (
  125. 'verify-script=',
  126. None,
  127. "Specify a script for the VERIFY phase of the RPM build",
  128. ),
  129. # Allow a packager to explicitly force an architecture
  130. ('force-arch=', None, "Force an architecture onto the RPM build process"),
  131. ('quiet', 'q', "Run the INSTALL phase of RPM building in quiet mode"),
  132. ]
  133. boolean_options: ClassVar[list[str]] = [
  134. 'keep-temp',
  135. 'use-rpm-opt-flags',
  136. 'rpm3-mode',
  137. 'no-autoreq',
  138. 'quiet',
  139. ]
  140. negative_opt: ClassVar[dict[str, str]] = {
  141. 'no-keep-temp': 'keep-temp',
  142. 'no-rpm-opt-flags': 'use-rpm-opt-flags',
  143. 'rpm2-mode': 'rpm3-mode',
  144. }
  145. def initialize_options(self):
  146. self.bdist_base = None
  147. self.rpm_base = None
  148. self.dist_dir = None
  149. self.python = None
  150. self.fix_python = None
  151. self.spec_only = None
  152. self.binary_only = None
  153. self.source_only = None
  154. self.use_bzip2 = None
  155. self.distribution_name = None
  156. self.group = None
  157. self.release = None
  158. self.serial = None
  159. self.vendor = None
  160. self.packager = None
  161. self.doc_files = None
  162. self.changelog = None
  163. self.icon = None
  164. self.prep_script = None
  165. self.build_script = None
  166. self.install_script = None
  167. self.clean_script = None
  168. self.verify_script = None
  169. self.pre_install = None
  170. self.post_install = None
  171. self.pre_uninstall = None
  172. self.post_uninstall = None
  173. self.prep = None
  174. self.provides = None
  175. self.requires = None
  176. self.conflicts = None
  177. self.build_requires = None
  178. self.obsoletes = None
  179. self.keep_temp = False
  180. self.use_rpm_opt_flags = True
  181. self.rpm3_mode = True
  182. self.no_autoreq = False
  183. self.force_arch = None
  184. self.quiet = False
  185. def finalize_options(self) -> None:
  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("you must specify --rpm-base in RPM 2 mode")
  190. self.rpm_base = os.path.join(self.bdist_base, "rpm")
  191. if self.python is None:
  192. if self.fix_python:
  193. self.python = sys.executable
  194. else:
  195. self.python = "python3"
  196. elif self.fix_python:
  197. raise DistutilsOptionError(
  198. "--python and --fix-python are mutually exclusive options"
  199. )
  200. if os.name != 'posix':
  201. raise DistutilsPlatformError(
  202. f"don't know how to create RPM distributions on platform {os.name}"
  203. )
  204. if self.binary_only and self.source_only:
  205. raise DistutilsOptionError(
  206. "cannot supply both '--source-only' and '--binary-only'"
  207. )
  208. # don't pass CFLAGS to pure python distributions
  209. if not self.distribution.has_ext_modules():
  210. self.use_rpm_opt_flags = False
  211. self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
  212. self.finalize_package_data()
  213. def finalize_package_data(self) -> None:
  214. self.ensure_string('group', "Development/Libraries")
  215. self.ensure_string(
  216. 'vendor',
  217. f"{self.distribution.get_contact()} <{self.distribution.get_contact_email()}>",
  218. )
  219. self.ensure_string('packager')
  220. self.ensure_string_list('doc_files')
  221. if isinstance(self.doc_files, list):
  222. for readme in ('README', 'README.txt'):
  223. if os.path.exists(readme) and readme not in self.doc_files:
  224. self.doc_files.append(readme)
  225. self.ensure_string('release', "1")
  226. self.ensure_string('serial') # should it be an int?
  227. self.ensure_string('distribution_name')
  228. self.ensure_string('changelog')
  229. # Format changelog correctly
  230. self.changelog = self._format_changelog(self.changelog)
  231. self.ensure_filename('icon')
  232. self.ensure_filename('prep_script')
  233. self.ensure_filename('build_script')
  234. self.ensure_filename('install_script')
  235. self.ensure_filename('clean_script')
  236. self.ensure_filename('verify_script')
  237. self.ensure_filename('pre_install')
  238. self.ensure_filename('post_install')
  239. self.ensure_filename('pre_uninstall')
  240. self.ensure_filename('post_uninstall')
  241. # XXX don't forget we punted on summaries and descriptions -- they
  242. # should be handled here eventually!
  243. # Now *this* is some meta-data that belongs in the setup script...
  244. self.ensure_string_list('provides')
  245. self.ensure_string_list('requires')
  246. self.ensure_string_list('conflicts')
  247. self.ensure_string_list('build_requires')
  248. self.ensure_string_list('obsoletes')
  249. self.ensure_string('force_arch')
  250. def run(self) -> None: # noqa: C901
  251. if DEBUG:
  252. print("before _get_package_data():")
  253. print("vendor =", self.vendor)
  254. print("packager =", self.packager)
  255. print("doc_files =", self.doc_files)
  256. print("changelog =", self.changelog)
  257. # make directories
  258. if self.spec_only:
  259. spec_dir = self.dist_dir
  260. self.mkpath(spec_dir)
  261. else:
  262. rpm_dir = {}
  263. for d in ('SOURCES', 'SPECS', 'BUILD', 'RPMS', 'SRPMS'):
  264. rpm_dir[d] = os.path.join(self.rpm_base, d)
  265. self.mkpath(rpm_dir[d])
  266. spec_dir = rpm_dir['SPECS']
  267. # Spec file goes into 'dist_dir' if '--spec-only specified',
  268. # build/rpm.<plat> otherwise.
  269. spec_path = os.path.join(spec_dir, f"{self.distribution.get_name()}.spec")
  270. self.execute(
  271. write_file, (spec_path, self._make_spec_file()), f"writing '{spec_path}'"
  272. )
  273. if self.spec_only: # stop if requested
  274. return
  275. # Make a source distribution and copy to SOURCES directory with
  276. # optional icon.
  277. saved_dist_files = self.distribution.dist_files[:]
  278. sdist = self.reinitialize_command('sdist')
  279. if self.use_bzip2:
  280. sdist.formats = ['bztar']
  281. else:
  282. sdist.formats = ['gztar']
  283. self.run_command('sdist')
  284. self.distribution.dist_files = saved_dist_files
  285. source = sdist.get_archive_files()[0]
  286. source_dir = rpm_dir['SOURCES']
  287. self.copy_file(source, source_dir)
  288. if self.icon:
  289. if os.path.exists(self.icon):
  290. self.copy_file(self.icon, source_dir)
  291. else:
  292. raise DistutilsFileError(f"icon file '{self.icon}' does not exist")
  293. # build package
  294. log.info("building RPMs")
  295. rpm_cmd = ['rpmbuild']
  296. if self.source_only: # what kind of RPMs?
  297. rpm_cmd.append('-bs')
  298. elif self.binary_only:
  299. rpm_cmd.append('-bb')
  300. else:
  301. rpm_cmd.append('-ba')
  302. rpm_cmd.extend(['--define', f'__python {self.python}'])
  303. if self.rpm3_mode:
  304. rpm_cmd.extend(['--define', f'_topdir {os.path.abspath(self.rpm_base)}'])
  305. if not self.keep_temp:
  306. rpm_cmd.append('--clean')
  307. if self.quiet:
  308. rpm_cmd.append('--quiet')
  309. rpm_cmd.append(spec_path)
  310. # Determine the binary rpm names that should be built out of this spec
  311. # file
  312. # Note that some of these may not be really built (if the file
  313. # list is empty)
  314. nvr_string = "%{name}-%{version}-%{release}"
  315. src_rpm = nvr_string + ".src.rpm"
  316. non_src_rpm = "%{arch}/" + nvr_string + ".%{arch}.rpm"
  317. q_cmd = rf"rpm -q --qf '{src_rpm} {non_src_rpm}\n' --specfile '{spec_path}'"
  318. out = os.popen(q_cmd)
  319. try:
  320. binary_rpms = []
  321. source_rpm = None
  322. while True:
  323. line = out.readline()
  324. if not line:
  325. break
  326. ell = line.strip().split()
  327. assert len(ell) == 2
  328. binary_rpms.append(ell[1])
  329. # The source rpm is named after the first entry in the spec file
  330. if source_rpm is None:
  331. source_rpm = ell[0]
  332. status = out.close()
  333. if status:
  334. raise DistutilsExecError(f"Failed to execute: {q_cmd!r}")
  335. finally:
  336. out.close()
  337. self.spawn(rpm_cmd)
  338. if self.distribution.has_ext_modules():
  339. pyversion = get_python_version()
  340. else:
  341. pyversion = 'any'
  342. if not self.binary_only:
  343. srpm = os.path.join(rpm_dir['SRPMS'], source_rpm)
  344. assert os.path.exists(srpm)
  345. self.move_file(srpm, self.dist_dir)
  346. filename = os.path.join(self.dist_dir, source_rpm)
  347. self.distribution.dist_files.append(('bdist_rpm', pyversion, filename))
  348. if not self.source_only:
  349. for rpm in binary_rpms:
  350. rpm = os.path.join(rpm_dir['RPMS'], rpm)
  351. if os.path.exists(rpm):
  352. self.move_file(rpm, self.dist_dir)
  353. filename = os.path.join(self.dist_dir, os.path.basename(rpm))
  354. self.distribution.dist_files.append((
  355. 'bdist_rpm',
  356. pyversion,
  357. filename,
  358. ))
  359. def _dist_path(self, path):
  360. return os.path.join(self.dist_dir, os.path.basename(path))
  361. def _make_spec_file(self): # noqa: C901
  362. """Generate the text of an RPM spec file and return it as a
  363. list of strings (one per line).
  364. """
  365. # definitions and headers
  366. spec_file = [
  367. '%define name ' + self.distribution.get_name(),
  368. '%define version ' + self.distribution.get_version().replace('-', '_'),
  369. '%define unmangled_version ' + self.distribution.get_version(),
  370. '%define release ' + self.release.replace('-', '_'),
  371. '',
  372. 'Summary: ' + (self.distribution.get_description() or "UNKNOWN"),
  373. ]
  374. # Workaround for #14443 which affects some RPM based systems such as
  375. # RHEL6 (and probably derivatives)
  376. vendor_hook = subprocess.getoutput('rpm --eval %{__os_install_post}')
  377. # Generate a potential replacement value for __os_install_post (whilst
  378. # normalizing the whitespace to simplify the test for whether the
  379. # invocation of brp-python-bytecompile passes in __python):
  380. vendor_hook = '\n'.join([
  381. f' {line.strip()} \\' for line in vendor_hook.splitlines()
  382. ])
  383. problem = "brp-python-bytecompile \\\n"
  384. fixed = "brp-python-bytecompile %{__python} \\\n"
  385. fixed_hook = vendor_hook.replace(problem, fixed)
  386. if fixed_hook != vendor_hook:
  387. spec_file.append('# Workaround for https://bugs.python.org/issue14443')
  388. spec_file.append('%define __os_install_post ' + fixed_hook + '\n')
  389. # put locale summaries into spec file
  390. # XXX not supported for now (hard to put a dictionary
  391. # in a config file -- arg!)
  392. # for locale in self.summaries.keys():
  393. # spec_file.append('Summary(%s): %s' % (locale,
  394. # self.summaries[locale]))
  395. spec_file.extend([
  396. 'Name: %{name}',
  397. 'Version: %{version}',
  398. 'Release: %{release}',
  399. ])
  400. # XXX yuck! this filename is available from the "sdist" command,
  401. # but only after it has run: and we create the spec file before
  402. # running "sdist", in case of --spec-only.
  403. if self.use_bzip2:
  404. spec_file.append('Source0: %{name}-%{unmangled_version}.tar.bz2')
  405. else:
  406. spec_file.append('Source0: %{name}-%{unmangled_version}.tar.gz')
  407. spec_file.extend([
  408. 'License: ' + (self.distribution.get_license() or "UNKNOWN"),
  409. 'Group: ' + self.group,
  410. 'BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot',
  411. 'Prefix: %{_prefix}',
  412. ])
  413. if not self.force_arch:
  414. # noarch if no extension modules
  415. if not self.distribution.has_ext_modules():
  416. spec_file.append('BuildArch: noarch')
  417. else:
  418. spec_file.append(f'BuildArch: {self.force_arch}')
  419. for field in (
  420. 'Vendor',
  421. 'Packager',
  422. 'Provides',
  423. 'Requires',
  424. 'Conflicts',
  425. 'Obsoletes',
  426. ):
  427. val = getattr(self, field.lower())
  428. if isinstance(val, list):
  429. spec_file.append('{}: {}'.format(field, ' '.join(val)))
  430. elif val is not None:
  431. spec_file.append(f'{field}: {val}')
  432. if self.distribution.get_url():
  433. spec_file.append('Url: ' + self.distribution.get_url())
  434. if self.distribution_name:
  435. spec_file.append('Distribution: ' + self.distribution_name)
  436. if self.build_requires:
  437. spec_file.append('BuildRequires: ' + ' '.join(self.build_requires))
  438. if self.icon:
  439. spec_file.append('Icon: ' + os.path.basename(self.icon))
  440. if self.no_autoreq:
  441. spec_file.append('AutoReq: 0')
  442. spec_file.extend([
  443. '',
  444. '%description',
  445. self.distribution.get_long_description() or "",
  446. ])
  447. # put locale descriptions into spec file
  448. # XXX again, suppressed because config file syntax doesn't
  449. # easily support this ;-(
  450. # for locale in self.descriptions.keys():
  451. # spec_file.extend([
  452. # '',
  453. # '%description -l ' + locale,
  454. # self.descriptions[locale],
  455. # ])
  456. # rpm scripts
  457. # figure out default build script
  458. def_setup_call = f"{self.python} {os.path.basename(sys.argv[0])}"
  459. def_build = f"{def_setup_call} build"
  460. if self.use_rpm_opt_flags:
  461. def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build
  462. # insert contents of files
  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. install_cmd = f'{def_setup_call} install -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES'
  467. script_options = [
  468. ('prep', 'prep_script', "%setup -n %{name}-%{unmangled_version}"),
  469. ('build', 'build_script', def_build),
  470. ('install', 'install_script', install_cmd),
  471. ('clean', 'clean_script', "rm -rf $RPM_BUILD_ROOT"),
  472. ('verifyscript', 'verify_script', None),
  473. ('pre', 'pre_install', None),
  474. ('post', 'post_install', None),
  475. ('preun', 'pre_uninstall', None),
  476. ('postun', 'post_uninstall', None),
  477. ]
  478. for rpm_opt, attr, default in script_options:
  479. # Insert contents of file referred to, if no file is referred to
  480. # use 'default' as contents of script
  481. val = getattr(self, attr)
  482. if val or default:
  483. spec_file.extend([
  484. '',
  485. '%' + rpm_opt,
  486. ])
  487. if val:
  488. with open(val) as f:
  489. spec_file.extend(f.read().split('\n'))
  490. else:
  491. spec_file.append(default)
  492. # files section
  493. spec_file.extend([
  494. '',
  495. '%files -f INSTALLED_FILES',
  496. '%defattr(-,root,root)',
  497. ])
  498. if self.doc_files:
  499. spec_file.append('%doc ' + ' '.join(self.doc_files))
  500. if self.changelog:
  501. spec_file.extend([
  502. '',
  503. '%changelog',
  504. ])
  505. spec_file.extend(self.changelog)
  506. return spec_file
  507. def _format_changelog(self, changelog):
  508. """Format the changelog correctly and convert it to a list of strings"""
  509. if not changelog:
  510. return changelog
  511. new_changelog = []
  512. for line in changelog.strip().split('\n'):
  513. line = line.strip()
  514. if line[0] == '*':
  515. new_changelog.extend(['', line])
  516. elif line[0] == '-':
  517. new_changelog.append(line)
  518. else:
  519. new_changelog.append(' ' + line)
  520. # strip trailing newline inserted by first changelog entry
  521. if not new_changelog[0]:
  522. del new_changelog[0]
  523. return new_changelog