Source code for libka.pkgedit.ka_control_file

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# pylint: disable=line-too-long
# kate: space-indent on; indent-width 4; replace-tabs on; indent-mode python; remove-trailing-space modified;
# vim: expandtab ts=4
# pylint: enable=line-too-long

############################################################################
#   Copyright © 2015-2024 José Manuel Santamaría Lema <panfaust@gmail.com> #
#                                                                          #
#   This program is free software; you can redistribute it and/or modify   #
#   it under the terms of the GNU General Public License as published by   #
#   the Free Software Foundation; either version 2 of the License, or      #
#   (at your option) any later version.                                    #
############################################################################

"""This module just provides the KAControlFile class"""

import tempfile
import os
import re
import shutil

from debian import deb822
from debian.debian_support import Version

from libka.pkgedit.wrap_control import wrap_field
from libka.ka_data_utils import read_json_data_file
from libka.utils.python_debian_warnings import ignore_python_debian_warnings
from libka.utils.python_debian_warnings import reset_python_debian_warnings

[docs] class KAControlFile(): """Class to represent a debian/control file""" def __init__(self, file_path='debian/control'): self._file_path = file_path self._control_file_parsed = False self._src_pkg = None self._bin_pkg_list = [] self._bin_pkg_map = {}
[docs] def sanitize(self): """Sanitizes a debian/control file so it won't trigger bugs from python-debian or wrap-and-sort, see, for instance: https://git.launchpad.net/~kubuntu-packagers/kubuntu-packaging/+git/cantor/commit/?id=7422c21f https://git.launchpad.net/~kubuntu-packagers/kubuntu-packaging/+git/cantor/commit/?id=a90b3e90""" #Open original and new control files orig_control_file = open(self._file_path, 'r') new_control_fd, new_control_path = tempfile.mkstemp(text=True) new_control_file = os.fdopen(new_control_fd, mode='w') for line in orig_control_file: #Replace lines containing only whitespaces or tabs with blank lines line = re.sub('^[ \t]*$', '', line) #Write the line to the new control file only if it's not a comment if re.match('^[ \t]*#', line) is None: new_control_file.write(line) #Close files orig_control_file.close() new_control_file.close() #Move and delete the temporary file shutil.copy(new_control_path, self._file_path) os.remove(new_control_path)
[docs] def parse_control(self): """Parses a control file""" self.sanitize() self._control_file_parsed = True control_file = deb822.Packages.iter_paragraphs(open(self._file_path, 'r')) self._bin_pkg_list = [] self._bin_pkg_map = {} for pkg in control_file: if 'Source' in pkg: self._src_pkg = pkg elif 'Package' in pkg: pkg_name = pkg['Package'] self._bin_pkg_list.append(pkg_name) self._bin_pkg_map[pkg_name] = pkg elif 'Tests' in pkg: #We treat autopktest names as binary packages, this is a bit hackish but #convenient for some things like version bumping pkg_name = pkg['Tests'] self._bin_pkg_list.append(pkg_name) self._bin_pkg_map[pkg_name] = pkg control_file.close()
[docs] def get_standards_version(self): """Returns the current Standards-Version as a Version object""" if not self._control_file_parsed: self.parse_control() result = Version("0") if 'Standards-Version' in self._src_pkg: result = Version(self._src_pkg['Standards-Version']) return result
[docs] def set_standards_version(self, new_value): """Sets the current Standards-Version""" if not self._control_file_parsed: self.parse_control() self._src_pkg['Standards-Version'] = str(new_value)
[docs] def set_kubuntu_maintainer_fields(self): """ Sets the maintainer fields for Kubuntu. Returns `True` if the file was actually changed. """ #Parse control file if it wasn't already if not self._control_file_parsed: self.parse_control() #Take note of the previous maintainer field value previous_maintainer_field = self._src_pkg['Maintainer'] #Set the desired value for the maintainer field kubuntu_maintainer_field = "Kubuntu Developers <kubuntu-devel@lists.ubuntu.com>" #Update the maintainers fields if needed file_changed = False if previous_maintainer_field != kubuntu_maintainer_field: #Re-create the source paragraph in debian/control src_pkg_copy = self._src_pkg.copy() #Iterate over the fields of the original source package paragraph #and re-create the source paragraph, but this time setting the Maintainer #field and right below it, the XSBC-Original-Maintainer field fields = self._src_pkg.keys() for field in fields: #Delete the field in question to add it to the bottom later del src_pkg_copy[field] if field != 'Maintainer': #Just add the field as it is to the bottom src_pkg_copy[field] = self._src_pkg[field] else: #Add to the bottom the Maintainer and XSBC-Original-Maintainer properly #set for Kubuntu src_pkg_copy['Maintainer'] = kubuntu_maintainer_field src_pkg_copy['XSBC-Original-Maintainer'] = previous_maintainer_field #Replace the orginal source paragraph with the copy self._src_pkg = src_pkg_copy #Mark the file as changed file_changed = True #Return result, True if the file had to be changed return file_changed
[docs] def set_kubuntu_vcs_fields(self, release_type, repo_name): """ Sets the vcs fields for Kubuntu. Returns `True` if the file was actually changed. """ #Parse control file if it wasn't already if not self._control_file_parsed: self.parse_control() #Take note of the current vcs fields values current_vcs_git_field = self._src_pkg['Vcs-Git'] current_vcs_browser_field = self._src_pkg['Vcs-Browser'] #Read JSON file with needed urls git_remotes = read_json_data_file('git-remotes.json') #Find out the desired values for the vcs fields kubuntu_vcs_git_field = (git_remotes['remotes']['kubuntu-anon'][release_type] % {'component': release_type, 'repo': repo_name}) kubuntu_vcs_browser_field = (git_remotes['vcs-browser']['kubuntu'][release_type] % {'component': release_type, 'repo': repo_name}) #Set the values of the vcs fields if needed control_file_changed = False if current_vcs_git_field != kubuntu_vcs_git_field: self._src_pkg['Vcs-Git'] = kubuntu_vcs_git_field control_file_changed = True if current_vcs_browser_field != kubuntu_vcs_browser_field: self._src_pkg['Vcs-Browser'] = kubuntu_vcs_browser_field control_file_changed = True #Return result, True if the file had to be changed return control_file_changed
[docs] def get_bin_packages(self): """Returns the list of binary package names in the control file""" if not self._control_file_parsed: self.parse_control() return self._bin_pkg_list
# Examples of deb822.PkgRelation.parse_relations() output # # "emacs | emacsen, make, debianutils (>= 1.7)" becomes # [ [ {'name': 'emacs'}, {'name': 'emacsen'} ], # [ {'name': 'make'} ], # [ {'name': 'debianutils', 'version': ('>=', '1.7')} ] ] # # "tcl8.4-dev, procps [!hurd-i386]" becomes # [ [ {'name': 'tcl8.4-dev'} ], # [ {'name': 'procps', 'arch': (false, 'hurd-i386')} ] ] # # "qttools5-dev-tools (>= 5.11.1~) <!nodoc>" becomes # [ [ {'name': 'qttools5-dev-tools', # 'archqual': None, # 'version': ('>=', '5.11.1~'), # 'arch': None, # 'restrictions': [[BuildRestriction(enabled=False, profile='nodoc')]] # } # ] ] # # pylint: disable=too-many-arguments # pylint: disable=too-many-locals # pylint: disable=too-many-nested-blocks # pylint: disable=too-many-branches
[docs] def remove_from_relation(self, package_to_change, relation, package_to_remove, operator=None, version=None): """This function removes `package_to_remove` from `relation` of `package_to_change`. It does nothing if we can't find `relation` in `package_to_change`. If you whish to alter the source package paragraph with this method, you may pass as `package_to_change` the special value ":source:". If `operator` or `version` are given, it will remove `package_to_remove` only if its operator and version match the values given by that parameters. Returns `True` if `package_to_remove` was actually removed from `relation`. Examples: * This code will remove 'libbar5' from the 'libfoo5' 'Breaks' relation: ka_control_file.remove_from_relations('libfoo5', 'Breaks', 'libbar5') * This code will remove 'libbar5' from the 'libfoo5' 'Breaks' relation **only** if the binary package 'libfoo5' has a 'Breaks' field like this: [...], libbar5 (<< 5.47), [...] ka_control_file.remove_from_relations('libfoo5', 'Breaks', 'libbar5', '<<', '5.47') * This code will remove `pkg-kde-tools` from 'Build-Depends': ka_control_file.remove_from_relation(package_to_change=':source:', relation='Build-Depends', package_to_remove='pkg-kde-tools') """ if package_to_change == ":source:": paragraph_to_change = self._src_pkg else: paragraph_to_change = self._bin_pkg_map[package_to_change] if relation in paragraph_to_change: #Get the relation in structured fashion ignore_python_debian_warnings() relation_structured = deb822.PkgRelation.parse_relations(paragraph_to_change[relation]) reset_python_debian_warnings() #Create a list of itrems to delete from the current relation list_to_delete = [] for i, alternatives_list in enumerate(relation_structured): for j, alternative_item in enumerate(alternatives_list): if alternative_item['name'] == package_to_remove: if alternative_item['version'] is not None: relop, relversion = alternative_item['version'] if (operator is None) or (operator == relop): if (version is None) or (version == relversion): list_to_delete.append((i, j)) else: if version is None: list_to_delete.append((i, j)) #Delete the items from the relation for i, j in list_to_delete: del relation_structured[i][j] #Delete empty lists. relation_structured = filter(None, relation_structured) #Convert relation to string and put it in the paragraph to change paragraph_to_change[relation] = deb822.PkgRelation.str(relation_structured) #Delete the relation completely if it's empty. if paragraph_to_change[relation].strip() == "": del paragraph_to_change[relation] #Put the modified paragraph where it belongs, to make the change effective if package_to_change == ":source:": self._src_pkg = paragraph_to_change else: self._bin_pkg_map[package_to_change] = paragraph_to_change #Return function result if list_to_delete: return True else: return False
# pylint: enable=too-many-arguments # pylint: enable=too-many-locals # pylint: enable=too-many-nested-blocks # pylint: enable=too-many-branches
[docs] def add_to_relation(self, package_to_change, relation, package_to_add): """ This method adds `package_to_add` in the `relation` of `package_to_change`. You may pass as `package_to_change` the special value ":source:" which will change the source package paragraph. """ #Find out the paragraph to change if package_to_change == ":source:": paragraph_to_change = self._src_pkg else: paragraph_to_change = self._bin_pkg_map[package_to_change] #Add package_to_add to package_to_change relation if relation in paragraph_to_change: package_to_add_found = False ignore_python_debian_warnings() relation_structured = deb822.PkgRelation.parse_relations(paragraph_to_change[relation]) reset_python_debian_warnings() for alternatives_list in relation_structured: for alternative in alternatives_list: if alternative['name'] == package_to_add: package_to_add_found = True break #If the package_to_add wasn't found, add it. if not package_to_add_found: paragraph_to_change[relation] += ", " + package_to_add else: paragraph_to_change[relation] = package_to_add #Put the modified paragraph where it belongs if package_to_change == ":source:": self._src_pkg = paragraph_to_change else: self._bin_pkg_map[package_to_change] = paragraph_to_change
[docs] def wrap_and_sort_field(self, package, field, short_indent=False, trailing_comma=True): """ Change `field` from `package` so it would look like if we ran [ka-]wrap-and-sort. The `package` parameter is either the name of a binary package or the special value ":source:" which would change the source package paragraph. """ #Find out the paragraph to change if package == ":source:": paragraph_to_change = self._src_pkg else: paragraph_to_change = self._bin_pkg_map[package] #Abort if we don't find the field if field not in paragraph_to_change: return #Wrap field wrap_field(paragraph_to_change, field, True, short_indent, trailing_comma) #Put the modified paragraph where it belongs if package == ":source:": self._src_pkg = paragraph_to_change else: self._bin_pkg_map[package] = paragraph_to_change
# pylint: disable=too-many-arguments
[docs] def bump_version(self, package_to_change, relation, package_to_bump, version, operator='>='): """Bump the version of `package_to_bump` in `package_to_change` to `version`""" #Find out the paragraph to change if package_to_change == ":source:": paragraph_to_change = self._src_pkg else: paragraph_to_change = self._bin_pkg_map[package_to_change] #Abort if we don't find the relation if relation not in paragraph_to_change: return #Bump package_to_bump in the relation ignore_python_debian_warnings() relation_structured = deb822.PkgRelation.parse_relations(paragraph_to_change[relation]) reset_python_debian_warnings() for alternatives_list in relation_structured: for alternative in alternatives_list: if alternative['name'] == package_to_bump: alternative['version'] = (operator, version) paragraph_to_change[relation] = deb822.PkgRelation.str(relation_structured) #Put the modified paragraph where it belongs if package_to_change == ":source:": self._src_pkg = paragraph_to_change else: self._bin_pkg_map[package_to_change] = paragraph_to_change
# pylint: enable=too-many-arguments
[docs] def bump_versions_with_map(self, package_to_change, relation, pkg_version_map): """ This method bumps all the versions given by `pkg_version_map` in `relation` of `package_to_change`. If `relation` is not found in the corresponding paragraph, calling this function will do nothing. """ #Find out the paragraph to change if package_to_change == ":source:": paragraph_to_change = self._src_pkg else: paragraph_to_change = self._bin_pkg_map[package_to_change] #Abort if we don't find the relation if relation not in paragraph_to_change: return #Bump versions with the versions given in pkg_version_map ignore_python_debian_warnings() relation_structured = deb822.PkgRelation.parse_relations(paragraph_to_change[relation]) reset_python_debian_warnings() for alternatives_list in relation_structured: for alternative in alternatives_list: pkg_name = alternative['name'] if pkg_name in pkg_version_map: alternative['version'] = ('>=', pkg_version_map[pkg_name]) paragraph_to_change[relation] = deb822.PkgRelation.str(relation_structured) #Put the modified paragraph where it belongs if package_to_change == ":source:": self._src_pkg = paragraph_to_change else: self._bin_pkg_map[package_to_change] = paragraph_to_change
# Examples of deb822.PkgRelation.parse_relations() output # # "emacs | emacsen, make, debianutils (>= 1.7)" becomes # [ [ {'name': 'emacs'}, {'name': 'emacsen'} ], # [ {'name': 'make'} ], # [ {'name': 'debianutils', 'version': ('>=', '1.7')} ] ] # # "tcl8.4-dev, procps [!hurd-i386]" becomes # [ [ {'name': 'tcl8.4-dev'} ], # [ {'name': 'procps', 'arch': (false, 'hurd-i386')} ] ] # # "qttools5-dev-tools (>= 5.11.1~) <!nodoc>" becomes # [ [ {'name': 'qttools5-dev-tools', # 'archqual': None, # 'version': ('>=', '5.11.1~'), # 'arch': None, # 'restrictions': [[BuildRestriction(enabled=False, profile='nodoc')]] # } # ] ] # # pylint: disable=too-many-nested-blocks # pylint: disable=too-many-branches
[docs] def remove_doc_build_depends(self): """ Calling this method will remove all `Build-Depends` and `Build-Depends-Indep` labelled as `<!nodoc>` in the control file. """ #Get the build depends in structured fashion ignore_python_debian_warnings() build_depends = deb822.PkgRelation.parse_relations(self._src_pkg['Build-Depends']) if 'Build-Depends-Indep' in self._src_pkg: bds_indep = deb822.PkgRelation.parse_relations(self._src_pkg['Build-Depends-Indep']) else: bds_indep = [] reset_python_debian_warnings() #This is the object representing the <!nodoc> restriction restriction_to_find = deb822.PkgRelation.BuildRestriction(enabled=False, profile='nodoc') #Find out the packages to remove in Build-Depends bds_packages_to_remove = [] for alternatives_list in build_depends: for alternative in alternatives_list: package_name = alternative['name'] restrictions = alternative['restrictions'] if restrictions is not None: for restrictions_list in restrictions: for restriction in restrictions_list: if restriction == restriction_to_find: bds_packages_to_remove.append(package_name) #Find out the packages to remove in Build-Depeds-Indep bds_indep_packages_to_remove = [] for alternatives_list in bds_indep: for alternative in alternatives_list: package_name = alternative['name'] restrictions = alternative['restrictions'] if restrictions is not None: for restrictions_list in restrictions: for restriction in restrictions_list: if restriction == restriction_to_find: bds_indep_packages_to_remove.append(package_name) #Remove the packages for package in bds_packages_to_remove: self.remove_from_relation(':source:', 'Build-Depends', package) for package in bds_indep_packages_to_remove: self.remove_from_relation(':source:', 'Build-Depends-Indep', package)
# pylint: enable=too-many-nested-blocks # pylint: enable=too-many-branches
[docs] def remove_debian_broken_breaks(self): #TODO pass
[docs] def dump(self): """Dumps all the changes made so far to hard disk""" #Open file to write new_control_file = open(self._file_path, "w") #Dump contents of source package (if any) if self._src_pkg is not None: new_control_file.write(self._src_pkg.dump()) new_control_file.write('\n') #Dump contents of binary packages/tests first_bin_pkg = True for bin_pkg_name in self._bin_pkg_list: if not first_bin_pkg: new_control_file.write('\n') new_control_file.write(self._bin_pkg_map[bin_pkg_name].dump()) first_bin_pkg = False #Close file new_control_file.close()