Files
aufbau2csv/venv3_12/Lib/site-packages/cx_Freeze/command/bdist_rpm.py

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")