@@ -154,6 +154,7 @@ def enabled(self):
154154class 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
184190class 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
342350def _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+
481533def _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
528656def generate_version_file (versioning_vars , dest ):
0 commit comments