Skip to content

Commit 3ff9b0c

Browse files
committed
cephadm/build: Add Debian package support for bundled dependencies
Extends the cephadm build script to support bundling dependencies from Debian packages in addition to pip and RPM packages. This allows building cephadm on Debian-based distributions using system packages. Key changes: - Add 'deb' to DependencyMode enum to enable Debian package mode - Implement _setup_deb() to configure Debian dependency requirements - Add _install_deb_deps() to orchestrate Debian package installation - Add _gather_deb_package_dirs() to parse Debian package file listings and locate Python package directories (handles both site-packages and dist-packages directories used by Debian) - Add _deps_from_deb() to extract Python dependencies from installed Debian packages using dpkg/apt-cache tools - Fix variable reference bug in _install_deps() (deps.mode -> config.deps_mode) The Debian implementation follows a similar pattern to the existing RPM support, using dpkg-query and dpkg -L to locate installed packages and their files, with special handling for Debian naming conventions (e.g., PyYAML -> python3-yaml). Signed-off-by: Kefu Chai <[email protected]>
1 parent 2c68c14 commit 3ff9b0c

File tree

1 file changed

+143
-15
lines changed

1 file changed

+143
-15
lines changed

src/cephadm/build.py

Lines changed: 143 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def enabled(self):
154154
class DependencyMode(enum.Enum):
155155
pip = enum.auto()
156156
rpm = enum.auto()
157+
deb = enum.auto()
157158
none = enum.auto()
158159

159160

@@ -169,6 +170,8 @@ def __init__(self, cli_args):
169170
self._setup_pip()
170171
elif self.deps_mode == DependencyMode.rpm:
171172
self._setup_rpm()
173+
elif self.deps_mode == DependencyMode.deb:
174+
self._setup_deb()
172175

173176
def _setup_pip(self):
174177
if self._maj_min == (3, 6):
@@ -180,6 +183,9 @@ def _setup_pip(self):
180183
def _setup_rpm(self):
181184
self.requirements = [InstallSpec(**v) for v in PY_REQUIREMENTS]
182185

186+
def _setup_deb(self):
187+
self.requirements = [InstallSpec(**v) for v in PY_REQUIREMENTS]
188+
183189

184190
class DependencyInfo:
185191
"""Type for tracking bundled dependencies."""
@@ -336,7 +342,9 @@ def _install_deps(tempdir, config):
336342
return _install_pip_deps(tempdir, config)
337343
if config.deps_mode == DependencyMode.rpm:
338344
return _install_rpm_deps(tempdir, config)
339-
raise ValueError(f'unexpected deps mode: {deps.mode}')
345+
if config.deps_mode == DependencyMode.deb:
346+
return _install_deb_deps(tempdir, config)
347+
raise ValueError(f'unexpected deps mode: {config.deps_mode}')
340348

341349

342350
def _install_pip_deps(tempdir, config):
@@ -437,7 +445,26 @@ def _install_rpm_deps(tempdir, config):
437445
return dinfo
438446

439447

440-
def _gather_rpm_package_dirs(paths):
448+
def _install_deb_deps(tempdir, config):
449+
log.info("Installing dependencies using Debian packages")
450+
dinfo = DependencyInfo(config)
451+
for pkg in config.requirements:
452+
log.info(f"Looking for debian package for: {pkg.name!r}")
453+
_deps_from_deb(tempdir, config, dinfo, pkg.name)
454+
return dinfo
455+
456+
457+
def _gather_package_dirs(paths, expected_parent_dir):
458+
"""Parse package file listing to find Python package directories.
459+
460+
Args:
461+
paths: List of file paths from package listing
462+
expected_parent_dir: Expected parent directory name (e.g., 'site-packages' for RPM,
463+
'dist-packages' for Debian)
464+
465+
Returns:
466+
Tuple of (metadata_dir, package_dirs)
467+
"""
441468
# = The easy way =
442469
# the top_level.txt file can be used to determine where the python packages
443470
# actually are. We need all of those and the meta-data dir (parent of
@@ -456,7 +483,7 @@ def _gather_rpm_package_dirs(paths):
456483
# = The hard way =
457484
# loop through the directories to find the .dist-info dir (containing the
458485
# mandatory METADATA file, according to the spec) and once we know the
459-
# location of dist info we find the sibling paths from the rpm listing
486+
# location of dist info we find the sibling paths from the package listing
460487
dist_info = None
461488
ppaths = []
462489
for path in paths:
@@ -467,9 +494,9 @@ def _gather_rpm_package_dirs(paths):
467494
break
468495
if not dist_info:
469496
raise ValueError('no .dist-info METADATA found')
470-
if not dist_info.parent.name == 'site-packages':
497+
if dist_info.parent.name != expected_parent_dir:
471498
raise ValueError(
472-
'unexpected parent directory (not site-packages):'
499+
f'unexpected parent directory (not {expected_parent_dir}):'
473500
f' {dist_info.parent.name}'
474501
)
475502
siblings = [
@@ -478,6 +505,31 @@ def _gather_rpm_package_dirs(paths):
478505
return dist_info, siblings
479506

480507

508+
def _copy_package_files(tempdir, paths, expected_parent_dir):
509+
"""Copy package files to the build directory.
510+
511+
Args:
512+
tempdir: Temporary directory to copy files to
513+
paths: List of file paths from package listing
514+
expected_parent_dir: Expected parent directory name per packaging convention:
515+
- 'site-packages' for RPM-based distributions
516+
- 'dist-packages' for Debian-based distributions
517+
518+
Returns:
519+
None
520+
"""
521+
meta_dir, pkg_dirs = _gather_package_dirs(paths, expected_parent_dir)
522+
meta_dest = tempdir / meta_dir.name
523+
log.info(f"Copying {meta_dir} to {meta_dest}")
524+
# copy the meta data directory
525+
shutil.copytree(meta_dir, meta_dest, ignore=_ignore_cephadmlib)
526+
# copy all the package directories
527+
for pkg_dir in pkg_dirs:
528+
pkg_dest = tempdir / pkg_dir.name
529+
log.info(f"Copying {pkg_dir} to {pkg_dest}")
530+
shutil.copytree(pkg_dir, pkg_dest, ignore=_ignore_cephadmlib)
531+
532+
481533
def _deps_from_rpm(tempdir, config, dinfo, pkg):
482534
# first, figure out what rpm provides a particular python lib
483535
dist = f'python3.{sys.version_info.minor}dist({pkg})'.lower()
@@ -513,16 +565,92 @@ def _deps_from_rpm(tempdir, config, dinfo, pkg):
513565
['rpm', '-ql', rpmname], check=True, stdout=subprocess.PIPE
514566
)
515567
paths = [l.decode('utf8') for l in res.stdout.splitlines()]
516-
meta_dir, pkg_dirs = _gather_rpm_package_dirs(paths)
517-
meta_dest = tempdir / meta_dir.name
518-
log.info(f"Copying {meta_dir} to {meta_dest}")
519-
# copy the meta data directory
520-
shutil.copytree(meta_dir, meta_dest, ignore=_ignore_cephadmlib)
521-
# copy all the package directories
522-
for pkg_dir in pkg_dirs:
523-
pkg_dest = tempdir / pkg_dir.name
524-
log.info(f"Copying {pkg_dir} to {pkg_dest}")
525-
shutil.copytree(pkg_dir, pkg_dest, ignore=_ignore_cephadmlib)
568+
# RPM-based distributions use 'site-packages' for Python packages
569+
_copy_package_files(tempdir, paths, 'site-packages')
570+
571+
572+
def _deps_from_deb(tempdir, config, dinfo, pkg):
573+
"""Extract Python dependencies from Debian packages.
574+
575+
Args:
576+
tempdir: Temporary directory to copy package files to
577+
config: Build configuration
578+
dinfo: DependencyInfo instance to track dependencies
579+
pkg: Python package name (e.g., 'MarkupSafe', 'Jinja2', 'PyYAML')
580+
"""
581+
# Convert Python package name to Debian package name
582+
# Python packages are typically named python3-<lowercase-name>
583+
# Handle special cases: PyYAML -> python3-yaml, MarkupSafe -> python3-markupsafe
584+
pkg_lower = pkg.lower()
585+
if pkg_lower == 'pyyaml':
586+
deb_pkg_name = 'python3-yaml'
587+
else:
588+
deb_pkg_name = f'python3-{pkg_lower}'
589+
590+
# First, try to find the package using apt-cache
591+
# This helps verify the package exists before trying to list its files
592+
try:
593+
res = subprocess.run(
594+
['apt-cache', 'show', deb_pkg_name],
595+
check=True,
596+
stdout=subprocess.PIPE,
597+
stderr=subprocess.PIPE,
598+
)
599+
except subprocess.CalledProcessError:
600+
# Package not found, try alternative naming
601+
log.warning(f"Package {deb_pkg_name} not found via apt-cache, trying dpkg -S")
602+
# Try to search for files that might belong to this package
603+
# Search for the Python module in site-packages
604+
search_pattern = f'/usr/lib/python3*/dist-packages/{pkg.lower()}*'
605+
try:
606+
res = subprocess.run(
607+
['dpkg', '-S', search_pattern],
608+
check=True,
609+
stdout=subprocess.PIPE,
610+
stderr=subprocess.PIPE,
611+
)
612+
# dpkg -S output format: "package: /path/to/file"
613+
deb_pkg_name = res.stdout.decode('utf8').split(':')[0].strip()
614+
except subprocess.CalledProcessError as err:
615+
log.error(f"Could not find Debian package for {pkg}")
616+
log.error(f"Tried: {deb_pkg_name} and pattern search")
617+
sys.exit(1)
618+
619+
# Get version information using dpkg-query
620+
try:
621+
res = subprocess.run(
622+
['dpkg-query', '-W', '-f=${Version}\\n', deb_pkg_name],
623+
check=True,
624+
stdout=subprocess.PIPE,
625+
)
626+
version = res.stdout.decode('utf8').strip()
627+
except subprocess.CalledProcessError as err:
628+
log.error(f"Could not query version for package {deb_pkg_name}: {err}")
629+
sys.exit(1)
630+
631+
log.info(f"Debian Package: {deb_pkg_name} (version: {version})")
632+
dinfo.add(
633+
pkg,
634+
deb_name=deb_pkg_name,
635+
version=version,
636+
package_source='deb',
637+
)
638+
639+
# Get the list of files provided by the Debian package
640+
try:
641+
res = subprocess.run(
642+
['dpkg', '-L', deb_pkg_name],
643+
check=True,
644+
stdout=subprocess.PIPE,
645+
)
646+
except subprocess.CalledProcessError as err:
647+
log.error(f"Could not list files for package {deb_pkg_name}: {err}")
648+
sys.exit(1)
649+
650+
paths = [l.decode('utf8') for l in res.stdout.splitlines()]
651+
# Debian-based distributions use 'dist-packages' for system-managed Python packages
652+
# per Debian Python Policy: https://www.debian.org/doc/packaging-manuals/python-policy/
653+
_copy_package_files(tempdir, paths, 'dist-packages')
526654

527655

528656
def generate_version_file(versioning_vars, dest):

0 commit comments

Comments
 (0)