563 lines
19 KiB
Python
563 lines
19 KiB
Python
"""Implements the 'bdist_rpm' command (create RPM binary distributions).
|
|
|
|
Borrowed from distutils.command.bdist_rpm of Python 3.10 and merged with
|
|
bdist_rpm subclass of cx_Freeze 6.10.
|
|
|
|
https://rpm.org/documentation.html
|
|
https://rpm-packaging-guide.github.io/
|
|
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import platform
|
|
import shutil
|
|
import sys
|
|
import tarfile
|
|
from subprocess import CalledProcessError, check_output
|
|
from sysconfig import get_python_version
|
|
from typing import ClassVar
|
|
|
|
from setuptools import Command
|
|
|
|
from cx_Freeze._compat import IS_CONDA
|
|
from cx_Freeze.exception import ExecError, FileError, PlatformError
|
|
|
|
__all__ = ["bdist_rpm"]
|
|
|
|
|
|
class bdist_rpm(Command):
|
|
"""Create an RPM distribution."""
|
|
|
|
description = "create an RPM distribution"
|
|
|
|
user_options: ClassVar[list[tuple[str, str | None, str]]] = [
|
|
(
|
|
"bdist-base=",
|
|
None,
|
|
"base directory for creating built distributions",
|
|
),
|
|
(
|
|
"rpm-base=",
|
|
None,
|
|
"base directory for creating RPMs "
|
|
'[defaults to "rpm" under "--bdist-base"]',
|
|
),
|
|
(
|
|
"dist-dir=",
|
|
"d",
|
|
"directory to put final RPM files in "
|
|
"(and .spec files if --spec-only)",
|
|
),
|
|
("spec-only", None, "only regenerate spec file"),
|
|
# More meta-data: too RPM-specific to put in the setup script,
|
|
# but needs to go in the .spec file -- so we make these options
|
|
# to "bdist_rpm". The idea is that packagers would put this
|
|
# info in pyproject.toml or setup.cfg, although they are of course free
|
|
# to supply it on the command line.
|
|
(
|
|
"distribution-name=",
|
|
None,
|
|
"name of the (Linux) distribution to which this "
|
|
"RPM applies (*not* the name of the module distribution!)",
|
|
),
|
|
(
|
|
"group=",
|
|
None,
|
|
'package classification [default: "Development/Libraries"]',
|
|
),
|
|
("release=", None, "RPM release number"),
|
|
("serial=", None, "RPM serial number"),
|
|
(
|
|
"vendor=",
|
|
None,
|
|
'RPM "vendor" (eg. "Joe Blow <joe@example.com>") '
|
|
"[default: maintainer or author from setup script]",
|
|
),
|
|
(
|
|
"packager=",
|
|
None,
|
|
'RPM packager (eg. "Jane Doe <jane@example.net>") '
|
|
"[default: same as vendor]",
|
|
),
|
|
(
|
|
"doc-files=",
|
|
None,
|
|
"list of documentation files (space or comma-separated)",
|
|
),
|
|
("changelog=", None, "RPM changelog"),
|
|
("icon=", None, "name of icon file"),
|
|
("provides=", None, "capabilities provided by this package"),
|
|
("requires=", None, "capabilities required by this package"),
|
|
("conflicts=", None, "capabilities which conflict with this package"),
|
|
(
|
|
"build-requires=",
|
|
None,
|
|
"capabilities required to build this package",
|
|
),
|
|
("obsoletes=", None, "capabilities made obsolete by this package"),
|
|
("no-autoreq", None, "do not automatically calculate dependencies"),
|
|
# Actions to take when building RPM
|
|
("keep-temp", "k", "don't clean up RPM build directory"),
|
|
("no-keep-temp", None, "clean up RPM build directory [default]"),
|
|
# Add the hooks necessary for specifying custom scripts
|
|
(
|
|
"prep-script=",
|
|
None,
|
|
"Specify a script for the PREP phase of RPM building",
|
|
),
|
|
(
|
|
"build-script=",
|
|
None,
|
|
"Specify a script for the BUILD phase of RPM building",
|
|
),
|
|
(
|
|
"pre-install=",
|
|
None,
|
|
"Specify a script for the pre-INSTALL phase of RPM building",
|
|
),
|
|
(
|
|
"install-script=",
|
|
None,
|
|
"Specify a script for the INSTALL phase of RPM building",
|
|
),
|
|
(
|
|
"post-install=",
|
|
None,
|
|
"Specify a script for the post-INSTALL phase of RPM building",
|
|
),
|
|
(
|
|
"pre-uninstall=",
|
|
None,
|
|
"Specify a script for the pre-UNINSTALL phase of RPM building",
|
|
),
|
|
(
|
|
"post-uninstall=",
|
|
None,
|
|
"Specify a script for the post-UNINSTALL phase of RPM building",
|
|
),
|
|
(
|
|
"clean-script=",
|
|
None,
|
|
"Specify a script for the CLEAN phase of RPM building",
|
|
),
|
|
(
|
|
"verify-script=",
|
|
None,
|
|
"Specify a script for the VERIFY phase of the RPM build",
|
|
),
|
|
("quiet", "q", "Run the INSTALL phase of RPM building in quiet mode"),
|
|
("debug", "g", "Run in debug mode"),
|
|
]
|
|
|
|
boolean_options: ClassVar[list[str]] = [
|
|
"keep-temp",
|
|
"no-autoreq",
|
|
"quiet",
|
|
"debug",
|
|
]
|
|
|
|
negative_opt: ClassVar[dict[str, str]] = {
|
|
"no-keep-temp": "keep-temp",
|
|
}
|
|
|
|
def initialize_options(self) -> None:
|
|
self.bdist_base = None
|
|
self.dist_dir = None
|
|
|
|
self.rpm_base = None
|
|
self.spec_only = None
|
|
|
|
self.distribution_name = None
|
|
self.group = None
|
|
self.release = None
|
|
self.serial = None
|
|
self.vendor = None
|
|
self.packager = None
|
|
self.doc_files = None
|
|
self.changelog = None
|
|
self.icon = None
|
|
|
|
self.prep_script = None
|
|
self.build_script = None
|
|
self.install_script = None
|
|
self.clean_script = None
|
|
self.verify_script = None
|
|
self.pre_install = None
|
|
self.post_install = None
|
|
self.pre_uninstall = None
|
|
self.post_uninstall = None
|
|
self.prep = None
|
|
self.provides = None
|
|
self.requires = None
|
|
self.conflicts = None
|
|
self.build_requires = None
|
|
self.obsoletes = None
|
|
|
|
self.keep_temp = 0
|
|
self.no_autoreq = 0
|
|
|
|
self.quiet = 0
|
|
self.debug = 0
|
|
|
|
def finalize_options(self) -> None:
|
|
if os.name != "posix":
|
|
msg = (
|
|
"don't know how to create RPM "
|
|
f"distributions on platform {os.name}"
|
|
)
|
|
raise PlatformError(msg)
|
|
|
|
self._rpm = shutil.which("rpm")
|
|
self._rpmbuild = shutil.which("rpmbuild")
|
|
if not self._rpmbuild:
|
|
msg = "failed to find rpmbuild for this platform."
|
|
raise PlatformError(msg)
|
|
|
|
self.set_undefined_options(
|
|
"bdist",
|
|
("bdist_base", "bdist_base"),
|
|
("dist_dir", "dist_dir"),
|
|
)
|
|
if self.rpm_base is None:
|
|
self.rpm_base = os.path.join(self.bdist_base, "rpm")
|
|
|
|
self.finalize_package_data()
|
|
|
|
def finalize_package_data(self) -> None:
|
|
self.ensure_string("group", "Development/Libraries")
|
|
contact = self.distribution.get_contact() or "UNKNOWN"
|
|
contact_email = self.distribution.get_contact_email() or "UNKNOWN"
|
|
self.ensure_string("vendor", f"{contact} <{contact_email}>")
|
|
self.ensure_string("packager")
|
|
self.ensure_string_list("doc_files")
|
|
if isinstance(self.doc_files, list):
|
|
doc_files = set(self.doc_files)
|
|
for readme in ("README", "README.txt"):
|
|
if os.path.exists(readme) and readme not in doc_files:
|
|
self.doc_files.append(readme)
|
|
|
|
self.ensure_string("release", "1")
|
|
self.ensure_string("serial") # should it be an int?
|
|
|
|
self.ensure_string("distribution_name")
|
|
|
|
self.ensure_string("changelog")
|
|
# Format changelog correctly
|
|
self.changelog = self._format_changelog(self.changelog)
|
|
|
|
self.ensure_filename("icon")
|
|
|
|
self.ensure_filename("prep_script")
|
|
self.ensure_filename("build_script")
|
|
self.ensure_filename("install_script")
|
|
self.ensure_filename("clean_script")
|
|
self.ensure_filename("verify_script")
|
|
self.ensure_filename("pre_install")
|
|
self.ensure_filename("post_install")
|
|
self.ensure_filename("pre_uninstall")
|
|
self.ensure_filename("post_uninstall")
|
|
|
|
# Now *this* is some meta-data that belongs in the setup script...
|
|
self.ensure_string_list("provides")
|
|
self.ensure_string_list("requires")
|
|
self.ensure_string_list("conflicts")
|
|
self.ensure_string_list("build_requires")
|
|
self.ensure_string_list("obsoletes")
|
|
|
|
def run(self) -> None:
|
|
if self.debug:
|
|
print("before _get_package_data():")
|
|
print("vendor =", self.vendor)
|
|
print("packager =", self.packager)
|
|
print("doc_files =", self.doc_files)
|
|
print("changelog =", self.changelog)
|
|
|
|
# make directories
|
|
if self.spec_only:
|
|
spec_dir = self.dist_dir
|
|
else:
|
|
rpm_dir = {}
|
|
for data in ("SOURCES", "SPECS", "BUILD", "RPMS", "SRPMS"):
|
|
rpm_dir[data] = os.path.join(self.rpm_base, data)
|
|
self.mkpath(rpm_dir[data])
|
|
spec_dir = rpm_dir["SPECS"]
|
|
self.mkpath(self.dist_dir)
|
|
|
|
# Spec file goes into 'dist_dir' if '--spec-only specified',
|
|
# build/rpm.<plat> otherwise.
|
|
distribution_name = self.distribution.get_name()
|
|
spec_path = os.path.join(spec_dir, f"{distribution_name}.spec")
|
|
self.execute(
|
|
write_file,
|
|
(spec_path, self._make_spec_file()),
|
|
f"writing '{spec_path}'",
|
|
)
|
|
|
|
if self.spec_only: # stop if requested
|
|
return
|
|
|
|
# Make a source distribution and copy to SOURCES directory with
|
|
# optional icon.
|
|
def exclude_filter(info: tarfile.TarInfo) -> tarfile.TarInfo | None:
|
|
if (
|
|
os.path.basename(info.name) in ("build", "dist")
|
|
and info.isdir()
|
|
):
|
|
return None
|
|
return info
|
|
|
|
name = self.distribution.get_name()
|
|
version = self.distribution.get_version()
|
|
source = f"{name}-{version}"
|
|
source_dir = rpm_dir["SOURCES"]
|
|
source_fullname = os.path.join(source_dir, source + ".tar.gz")
|
|
with tarfile.open(source_fullname, "w:gz") as tar:
|
|
tar.add(".", source, filter=exclude_filter)
|
|
if self.icon:
|
|
if os.path.exists(self.icon):
|
|
self.copy_file(self.icon, source_dir)
|
|
else:
|
|
msg = f"icon file {self.icon!r} does not exist"
|
|
raise FileError(msg)
|
|
|
|
# build package, binary only (-bb)
|
|
logging.info("building RPMs")
|
|
rpm_cmd = [self._rpmbuild, "-bb"]
|
|
if not self.keep_temp:
|
|
rpm_cmd.append("--clean")
|
|
|
|
if self.quiet:
|
|
rpm_cmd.append("--quiet")
|
|
|
|
rpm_cmd.append(spec_path)
|
|
# Determine the binary rpm names that should be built out of this spec
|
|
# file
|
|
# Note that some of these may not be really built (if the file
|
|
# list is empty)
|
|
nvr_string = "%{name}-%{version}-%{release}"
|
|
src_rpm = nvr_string + ".src.rpm"
|
|
non_src_rpm = "%{arch}/" + nvr_string + ".%{arch}.rpm"
|
|
q_cmd = [
|
|
self._rpm,
|
|
"-q",
|
|
"--qf",
|
|
rf"{src_rpm} {non_src_rpm}\n",
|
|
"--specfile",
|
|
spec_path,
|
|
]
|
|
try:
|
|
out = check_output(q_cmd, text=True)
|
|
except CalledProcessError as exc:
|
|
msg = f"Failed to execute: {' '.join(q_cmd)!r}"
|
|
raise ExecError(msg) from exc
|
|
|
|
binary_rpms = []
|
|
for line in out.splitlines():
|
|
rows = line.split()
|
|
assert len(rows) == 2 # noqa: S101
|
|
binary_rpms.append(rows[1])
|
|
|
|
self.spawn(rpm_cmd)
|
|
|
|
if not self.dry_run:
|
|
pyversion = get_python_version()
|
|
|
|
for binary_rpm in binary_rpms:
|
|
rpm = os.path.join(rpm_dir["RPMS"], binary_rpm)
|
|
if os.path.exists(rpm):
|
|
self.move_file(rpm, self.dist_dir)
|
|
filename = os.path.join(
|
|
self.dist_dir, os.path.basename(rpm)
|
|
)
|
|
self.distribution.dist_files.append(
|
|
("bdist_rpm", pyversion, filename)
|
|
)
|
|
|
|
def _make_spec_file(self) -> list[str]:
|
|
"""Generate the text of an RPM spec file and return it as a
|
|
list of strings (one per line).
|
|
"""
|
|
# definitions and headers
|
|
dist = self.distribution
|
|
spec_file = [
|
|
f"%define _topdir {os.path.abspath(self.rpm_base)}",
|
|
# cx_Freeze specific
|
|
"%define __prelink_undo_cmd %{nil}",
|
|
"%define __strip /bin/true",
|
|
"",
|
|
f"%define name {dist.get_name()}",
|
|
f"%define version {dist.get_version().replace('-', '_')}",
|
|
f"%define unmangled_version {dist.get_version()}",
|
|
f"%define release {self.release.replace('-', '_')}",
|
|
"",
|
|
f"Summary: {dist.get_description() or 'UNKNOWN'}",
|
|
"Name: %{name}",
|
|
"Version: %{version}",
|
|
"Release: %{release}",
|
|
f"License: {dist.get_license() or 'UNKNOWN'}",
|
|
f"Group: {self.group}",
|
|
"BuildRoot: %{buildroot}",
|
|
"Prefix: %{_prefix}",
|
|
f"BuildArch: {platform.machine()}",
|
|
]
|
|
|
|
# Fix for conda
|
|
if IS_CONDA:
|
|
spec_file.append("%define debug_package %{nil}")
|
|
|
|
# Workaround for #14443 which affects some RPM based systems such as
|
|
# RHEL6 (and probably derivatives)
|
|
vendor_hook = check_output(
|
|
[self._rpm, "--eval", "%{__os_install_post}"], text=True
|
|
)
|
|
# Generate a potential replacement value for __os_install_post (whilst
|
|
# normalizing the whitespace to simplify the test for whether the
|
|
# invocation of brp-python-bytecompile passes in __python):
|
|
vendor_hook = "\n".join(
|
|
[f" {line.strip()} \\" for line in vendor_hook.splitlines()]
|
|
)
|
|
problem = "brp-python-bytecompile \\\n"
|
|
fixed = "brp-python-bytecompile %{__python} \\\n"
|
|
fixed_hook = vendor_hook.replace(problem, fixed)
|
|
if fixed_hook != vendor_hook:
|
|
spec_file += [
|
|
"# Workaround for http://bugs.python.org/issue14443",
|
|
f"%define __python {sys.executable}",
|
|
f"%define __os_install_post {fixed_hook}",
|
|
"",
|
|
]
|
|
|
|
# we create the spec file before running 'tar' in case of --spec-only.
|
|
spec_file.append("Source0: %{name}-%{unmangled_version}.tar.gz")
|
|
|
|
for field in (
|
|
"Vendor",
|
|
"Packager",
|
|
"Provides",
|
|
"Requires",
|
|
"Conflicts",
|
|
"Obsoletes",
|
|
):
|
|
val = getattr(self, field.lower())
|
|
if isinstance(val, list):
|
|
join_val = " ".join(val)
|
|
spec_file.append(f"{field}: {join_val}")
|
|
elif val is not None:
|
|
spec_file.append(f"{field}: {val}")
|
|
|
|
if dist.get_url() not in (None, "UNKNOWN"):
|
|
spec_file.append(f"Url: {dist.get_url()}")
|
|
|
|
if self.distribution_name:
|
|
spec_file.append(f"Distribution: {self.distribution_name}")
|
|
|
|
if self.build_requires:
|
|
spec_file.append("BuildRequires: " + " ".join(self.build_requires))
|
|
|
|
if self.icon:
|
|
spec_file.append("Icon: " + os.path.basename(self.icon))
|
|
|
|
if self.no_autoreq:
|
|
spec_file.append("AutoReq: 0")
|
|
|
|
spec_file += [
|
|
"",
|
|
"%description",
|
|
dist.get_long_description() or dist.get_description() or "UNKNOWN",
|
|
]
|
|
|
|
# rpm scripts - figure out default build script
|
|
if dist.script_name == "cxfreeze":
|
|
def_setup_call = shutil.which(dist.script_name)
|
|
else:
|
|
def_setup_call = f"{sys.executable} {dist.script_name}"
|
|
def_build = f"{def_setup_call} build_exe --optimize=1 --silent"
|
|
def_build = 'env CFLAGS="$RPM_OPT_FLAGS" ' + def_build
|
|
|
|
# insert contents of files
|
|
|
|
# this is kind of misleading: user-supplied options are files
|
|
# that we open and interpolate into the spec file, but the defaults
|
|
# are just text that we drop in as-is.
|
|
|
|
install_cmd = (
|
|
f"{def_setup_call} install --skip-build"
|
|
" --prefix=%{_prefix} --root=%{buildroot}"
|
|
)
|
|
|
|
script_options = [
|
|
("prep", "prep_script", "%setup -n %{name}-%{unmangled_version}"),
|
|
("build", "build_script", def_build),
|
|
("install", "install_script", install_cmd),
|
|
("clean", "clean_script", "rm -rf %{buildroot}"),
|
|
("verifyscript", "verify_script", None),
|
|
("pre", "pre_install", None),
|
|
("post", "post_install", None),
|
|
("preun", "pre_uninstall", None),
|
|
("postun", "post_uninstall", None),
|
|
]
|
|
|
|
for rpm_opt, attr, default in script_options:
|
|
# Insert contents of file referred to, if no file is referred to
|
|
# use 'default' as contents of script
|
|
val = getattr(self, attr)
|
|
if val or default:
|
|
spec_file.extend(["", "%" + rpm_opt])
|
|
if val:
|
|
with open(val, encoding="utf_8") as file:
|
|
spec_file.extend(file.read().split("\n"))
|
|
else:
|
|
spec_file.append(default)
|
|
|
|
# files section
|
|
spec_file += [
|
|
"",
|
|
"%files",
|
|
"%dir %{_prefix}/lib/%{name}-%{unmangled_version}",
|
|
"%{_prefix}/lib/%{name}-%{unmangled_version}/*",
|
|
"%{_bindir}/%{name}",
|
|
"%defattr(-,root,root)",
|
|
]
|
|
|
|
if self.doc_files:
|
|
spec_file.append("%doc " + " ".join(self.doc_files))
|
|
|
|
if self.changelog:
|
|
spec_file.extend(["", "%changelog"])
|
|
spec_file.extend(self.changelog)
|
|
|
|
return spec_file
|
|
|
|
@staticmethod
|
|
def _format_changelog(changelog) -> list[str]:
|
|
"""Format the changelog correctly and convert it to a string list."""
|
|
if not changelog:
|
|
return changelog
|
|
new_changelog = []
|
|
for raw_line in changelog.strip().split("\n"):
|
|
line = raw_line.strip()
|
|
if line[0] == "*":
|
|
new_changelog.extend(["", line])
|
|
elif line[0] == "-":
|
|
new_changelog.append(line)
|
|
else:
|
|
new_changelog.append(" " + line)
|
|
|
|
# strip trailing newline inserted by first changelog entry
|
|
if not new_changelog[0]:
|
|
del new_changelog[0]
|
|
|
|
return new_changelog
|
|
|
|
|
|
def write_file(filename, contents) -> None:
|
|
"""Create a file with the specified name and write 'contents'
|
|
(a sequence of strings without line terminators) to it.
|
|
"""
|
|
with open(filename, "w", encoding="utf_8") as file:
|
|
for line in contents:
|
|
file.write(line + "\n")
|