18 Star 1 Fork 3

openKylin / packaging-tools

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
apt-depends.py 17.56 KB
一键复制 编辑 原始数据 按行查看 历史
周淦清 提交于 2023-03-08 09:32 . Function structure optimization
#!/usr/bin/env python3
import argparse
import apt_pkg
import json
import sys
cache = None
dep_cache = None
sr = None
_pkg_cache = set()
global_install_list = set() # 保存全局待安装的软件包列表 (目标软件包 + 安装依赖链)
global_broken_list = set() # 保存全局的Breaks和Conflicts关系
class NoPackage(Exception):
def __init__(self, *args: object) -> None:
super().__init__(*args)
class NoCandidatePackage(Exception):
def __init__(self, *args: object) -> None:
super().__init__(*args)
class NoInstalledPackage(Exception):
def __init__(self, *args: object) -> None:
super().__init__(*args)
class NoInstallablePackage(Exception):
def __init__(self, *args: object) -> None:
super().__init__(*args)
# 对输入的软件包进行虚包类型和无版本检测
def check_pkg_is_virtual_or_no_version(pkg: apt_pkg.Package) -> bool:
"""
返回值: True表示检测到虚包或无版本的情况; 默认值为False,表示检测通过.
下方关键属性解释:
apt_pkg.Package.has_provides: Whether the package is provided by at least one other package.
apt_pkg.Package.has_versions: Whether the package has at least one version in the cache.
apt_pkg.Package.provides_list: A list of all packages providing this package. The list contains
tuples in the format (providesname, providesver, version)
where 'version' is an apt_pkg.Version object.
"""
# 判断是虚包的情况
if pkg.has_provides and not pkg.has_versions:
true_package_list = set() # 使用集合去重
for provides in pkg.provides_list:
_, _, _version = provides
true_package_list.add(_version.parent_pkg.name)
print(f'{pkg.name} 是虚包, 由 {" / ".join(true_package_list)} 提供')
return True
# 不是虚包,但是源里也没有任何版本
elif not pkg.has_provides and not pkg.has_versions:
print(f'{pkg.name} 不是虚包,但是源里也没有任何版本')
return True
return False
# 根据输入参数获取apt_pkg.Version对象
def get_pkg_version(pkg: apt_pkg.Package, ver_str: str, installed: bool) -> apt_pkg.Version:
version = None
# 指定版本号的情况
if ver_str:
# 查找对应的版本号
for ver in pkg.version_list:
if ver.ver_str == ver_str:
version = ver
# 检查是否有对应的版本号
if version is None:
raise NoPackage(f"No version {ver_str} for {pkg.name}")
# 没有指定版本号的情况
else:
# 默认检查Candidate版本
if installed is False:
version = dep_cache.get_candidate_ver(pkg)
if version is None:
raise NoCandidatePackage(f"No candidate version for {pkg.name}")
# 已声明选择检查已安装的版本
else:
version = pkg.current_ver
if version is None:
raise NoInstalledPackage(f"No installed version for {pkg.name}")
return version
# 输出当前软件包的安装依赖
def print_pkg_depends(version: apt_pkg.Version):
for deps in version.depends_list_str.get("PreDepends", []) + \
version.depends_list_str.get("Depends", []):
deps_str = []
for dep in deps:
# dep: 3-tuples of the form (name, version, operator)
# operator is one of '<', '<=', '=', '>=', '>'.
_name, _version, _operator = dep
if _version or _operator:
deps_str.append(f"{_name} ({_operator} {_version})")
else:
deps_str.append(f"{_name}")
_show = " | ".join(deps_str)
print(f" Depends: {_show}")
# 输出当前软件包的冲突项 (Breaks & Conflicts)
def print_pkg_broken(version: apt_pkg.Version) -> set:
broken_dep_set = set() # 使用集合去重
for broken_deps in version.depends_list_str.get("Conflicts", []) + \
version.depends_list_str.get("Breaks", []):
for broken_dep in broken_deps:
broken_dep_set.add(broken_dep)
for broken_dep in broken_dep_set:
_name, _version, _operator = broken_dep
if _version or _operator:
broken_dep_str = f"{_name} ({_operator} {_version})"
else:
broken_dep_str = f"{_name}"
print(f" Broken: {broken_dep_str}")
return broken_dep_set
# 检查当前软件包是否与全局列表冲突列表是否有破坏性依赖关系
def check_pkg_with_global_broken_list(
version: apt_pkg.Version, is_optional: bool, broken_dep_set: set) -> bool:
for _name, _version, _operator in global_broken_list:
if _name == version.parent_pkg.name:
# 满足破坏性依赖项时,不能正常安装
if apt_pkg.check_dep(version.ver_str, _operator, _version) is True:
if is_optional is False:
raise NoCandidatePackage(
f"## Target package [{version.parent_pkg.name}/{version.ver_str}] "
f"not satisfy Breaks/Conflicts ({_operator} {_version})"
)
else:
# 在全局列表冲突列表中,去掉刚才添加的Breaks/Conflicts项
global_broken_list.difference_update(broken_dep_set)
return False
return True
# 根据依赖项包名获取apt_pkg.Package对象
def get_dep_pkg(package_name: str, is_optional: bool):
try:
dep_pkg = cache[package_name]
except KeyError:
if is_optional is True:
return None
else:
raise NoPackage(f"## Not exists {package_name}")
# 安装依赖是虚包的情况
if dep_pkg.has_provides and not dep_pkg.has_versions:
_, _, _version = dep_pkg.provides_list[0]
dep_pkg = _version.parent_pkg
return dep_pkg
# 检查当前软件包的Breaks和Conflicts字段是否与全局待安装列表冲突
def check_pkg_broken_with_global_install_list(
version: apt_pkg.Version, is_optional: bool) -> bool:
for broken_deps in version.depends_list.get("Conflicts", []) + \
version.depends_list.get("Breaks", []):
for broken_dep in broken_deps:
broken_package_name = broken_dep.target_pkg.name.split(":")[0]
for name_version in global_install_list:
if name_version[0] == broken_package_name:
# 满足破坏性依赖项时,不能正常安装
if apt_pkg.check_dep(name_version[1], broken_dep.comp_type, broken_dep.target_ver) is True:
if is_optional is False:
raise NoCandidatePackage(
f"## Candidate package [{name_version[0]}/{name_version[1]}] "
f"not satisfy Breaks/Conflicts ({broken_dep.comp_type} {broken_dep.target_ver})"
)
else:
return False
return True
def check_depends(
pkg: apt_pkg.Package,
ver_str: str = "",
installed: bool = False,
is_optional: bool = False,
check_broken: bool = False,
):
"""check dependencies by the given package
Args:
pkg (apt_pkg.Package): apt_pkg Package object
ver_str (str, optional): package version str. Defaults to "".
installed (bool, optional): True for check installed package, \
False for check candidate version. Defaults to False.
is_optional (bool, optional): whether the current package is optional. \
If True and dependencies are unsatisfied, then raise Exception. Defaults to False.
check_broken (bool, optional): whether the current package needs to be checked broken relationships. \
If True and dependencies are unsatisfied, then raise Exception. Defaults to False.
Raises:
NoPackage: no package
NoCandidatePackage: no candidate package
print: show package info
Returns:
None: no return
True: optional branch check passed
False: optional branch check failed
"""
# 当输入的包名有重复时, 防止重复执行和输出
if pkg.name not in _pkg_cache:
_pkg_cache.add(pkg.name)
else:
return
# 输出当前包名
print(f"{pkg.name}:")
# 对输入的软件包进行虚包类型和无版本检测
if check_pkg_is_virtual_or_no_version(pkg) is True:
return
# 根据输入参数获取apt_pkg.Version对象
version = get_pkg_version(pkg, ver_str, installed)
# 通过上一步基础检查后可以添加进全局安装列表
global_install_list.add((pkg.name, version.ver_str))
# 输出当前软件包的安装依赖
print_pkg_depends(version)
broken_dep_set = set() # 使用集合去重
if check_broken is True:
# 输出当前软件包的冲突项 (Breaks & Conflicts)
broken_dep_set = print_pkg_broken(version)
# 将当前软件包的冲突项加入全局列表
global_broken_list.update(broken_dep_set)
# 检查当前软件包是否与全局列表冲突列表是否有破坏性依赖关系
if check_pkg_with_global_broken_list(
version, is_optional, broken_dep_set) is False:
return False
# is_optional变量可能在下方会被修改,缓存修改前的值
is_optional_old = is_optional
# 遍历安装依赖项, deps: list[apt_pkg.Dependency]
for deps in version.depends_list.get("PreDepends", []) + \
version.depends_list.get("Depends", []):
branch_failed_count = 0
optional_pkg_list = []
# 判断此项安装依赖是否为多选一
if len(deps) > 1:
is_optional = True
for dep in deps:
package_name = dep.target_pkg.name.split(":")[0]
if is_optional is True:
optional_pkg_list.append(package_name)
# 根据依赖项包名获取apt_pkg.Package对象
dep_pkg = get_dep_pkg(package_name, is_optional)
if dep_pkg is None and is_optional is True:
branch_failed_count += 1
continue
# 获取候选版本
try:
candidate_pkg = dep_cache.get_candidate_ver(dep_pkg)
except:
raise NoCandidatePackage(f"## no candidate package for {dep_pkg.name}")
if candidate_pkg is None:
if is_optional is False:
raise NoCandidatePackage(f"## No candidate version for {dep_pkg.name}")
else:
branch_failed_count += 1
continue
# 检查候选版本是否满足安装依赖
if apt_pkg.check_dep(candidate_pkg.ver_str, dep.comp_type, dep.target_ver) is False:
if is_optional is False:
raise NoCandidatePackage(
f"## Candidate package [{candidate_pkg.parent_pkg.name}/{candidate_pkg.ver_str}] "
f"not satisfy requirements ({dep.comp_type} {dep.target_ver})"
)
else:
branch_failed_count += 1
continue
# 检查当前软件包的Breaks和Conflicts字段是否与全局待安装列表冲突
if check_broken is True:
check_result = check_pkg_broken_with_global_install_list(version, is_optional)
if check_result is False and is_optional is True:
branch_failed_count += 1
continue
# 递归调用,依次检查安装依赖链
return_value = check_depends(pkg=dep.target_pkg, is_optional=is_optional, check_broken=check_broken)
# print(f'[return_value] is {return_value}, {pkg.name} -> {dep.target_pkg.name}') # 调试用
# 传递某条依赖链不能正常安装的消息
if return_value is False:
# 不能安装的依赖链应该从全局包列表去掉
_pkg_cache.discard(dep.target_pkg.name)
# 在遍历集合时删除此集合内的元素时应该使用copy()方法,否则会报错:
# RuntimeError: Set changed size during iteration
for item in global_install_list.copy():
if item[0] == dep.target_pkg.name:
global_install_list.discard(item)
# 传递某条依赖链不能正常安装的消息
if is_optional_old is True:
# 在全局列表冲突列表中,去掉开头添加的Breaks/Conflicts项
if check_broken is True:
global_broken_list.difference_update(broken_dep_set)
return False
else:
branch_failed_count += 1
continue
# 将is_optional的值复位
is_optional = is_optional_old
# 当前可选依赖中的所有安装项全部都不满足依赖的情况
if branch_failed_count == len(deps):
if is_optional_old is True:
return False
else:
raise NoInstallablePackage(f'## [{" | ".join(optional_pkg_list)}] are not installable')
def get_true_pkg(pkg: str) -> str:
pkg_name = ""
try:
package = cache[pkg]
# 虚包
if package.has_provides and not package.has_versions:
_, _, true_pkg = package.provides_list[0]
pkg_name = true_pkg.parent_pkg.name
else:
pkg_name = pkg
finally:
return pkg_name
def get_recode(pkg) -> bool:
sr.restart()
return sr.lookup(pkg)
pkg_cache = []
def get_source_dependencies(pkg: str, ver_str: str = "", result: dict = {}, *keys) -> dict:
pkg = get_true_pkg(pkg=pkg)
if not pkg:
return {pkg: {"source": "", "version": ""}}
if pkg not in pkg_cache:
pkg_cache.append(pkg)
print(pkg)
else:
return {}
'''
{
"pkg_name": {
"required": {
"comp": ">",
"version": "1.0.0"
},
"source": "",
"version": "",
}
}
'''
keys = ["Build-Depends", "Build-Depends-Indep"]
if not get_recode(pkg):
return {pkg: {"source": "", "version": ""}}
build_depends = sr.build_depends
if not build_depends:
return {}
for key in keys:
for deps in build_depends.get(key, []):
for dep in deps:
# dep: pkg_name version comp
# print(dep)
pkg_name, version, comp = dep
pkg_name = get_true_pkg(pkg=pkg_name)
if not pkg_name or result.get(pkg_name):
continue
if not get_recode(pkg_name):
return {}
try:
_r = {pkg_name: {"required": {"comp": comp, "version": version}, "source": sr.package, "version": sr.version}}
except Exception as e:
print(pkg_name)
raise e
result.update(_r)
result.update(get_source_dependencies(pkg=pkg_name, result=result))
return result
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"-b",
"--build-depends",
dest="build_depends",
action="store_true",
default=False,
help="获取编译依赖链",
)
# 将输入的包名自动转为字符串列表
parser.add_argument(
"--package",
dest='package',
nargs="+",
type=str,
default=[],
help="二进制包名称",
)
# 以下两个参数应该设置为互斥关系,不可同时输入
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-e",
"--version",
dest="version",
default="",
help="软件包的版本号,如果为空会根据installed参数选择已安装版本或者是candidate版本",
)
group.add_argument(
"-i",
"--installed",
dest="installed",
action="store_true",
default=False,
help="检查已安装的版本",
)
args = parser.parse_args()
apt_pkg.init()
cache = apt_pkg.Cache() # 加载本地apt缓存文件
dep_cache = apt_pkg.DepCache(cache=cache)
if args.build_depends:
sr = apt_pkg.SourceRecords()
result = {}
for search_package in args.package:
result = get_source_dependencies(search_package)
with open(f"test.json", 'w') as f:
json.dump(result, f, indent=2)
sys.exit(0)
# 单包检查
if args.package:
for search_package in args.package:
# 判断输入的(二进制包)包名是否在本地缓存的包列表中
if search_package in cache:
package = cache[search_package]
check_depends(package, args.version, args.installed,
check_broken=True)
else:
print(f"## no package named by {search_package}")
sys.exit(1)
# 整个仓库的所有软件包检查
else:
for package in cache.packages:
# check_depends(package, check_broken=True)
try:
check_depends(package, check_broken=True)
except Exception as e:
print(f"[{package.name}] failure with: {e}")
sys.exit(1)
# 调试用
# print(len(_pkg_cache), len(global_install_list), len(global_broken_list))
# for i in global_broken_list:
# print(type(i), i)
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/openkylin/packaging-tools.git
git@gitee.com:openkylin/packaging-tools.git
openkylin
packaging-tools
packaging-tools
master

搜索帮助

344bd9b3 5694891 D2dac590 5694891