From f62662a7b17689256dd7f0bfa1d7d2b3bb75d614 Mon Sep 17 00:00:00 2001 From: tinaaaaa42 <805210884@qq.com> Date: Tue, 31 Oct 2023 16:20:42 +0800 Subject: [PATCH] Add deb2rpm to tools --- tools/deb2rpm/.gitignore | 7 + tools/deb2rpm/README.en.md | 36 ++ tools/deb2rpm/README.md | 53 ++ tools/deb2rpm/config/deb2rpm_mapping.json | 76 +++ tools/deb2rpm/config/resources.json | 79 +++ tools/deb2rpm/deb2rpm.py | 264 ++++++++++ tools/deb2rpm/deb2rpm_manager.py | 150 ++++++ tools/deb2rpm/demos/deb2rpm/conf/config.yaml | 0 tools/deb2rpm/demos/deb2rpm/conf/logger.conf | 0 .../demos/deb2rpm/conf/pkg_name_maaping.json | 0 tools/deb2rpm/demos/deb2rpm/deb2rpm.py | 58 +++ tools/deb2rpm/demos/deb2rpm/src/data.py | 32 ++ tools/deb2rpm/demos/deb2rpm/src/package.py | 0 tools/deb2rpm/demos/deb2rpm/util/command.py | 0 .../deb2rpm/demos/deb2rpm/util/deb_helper.py | 0 tools/deb2rpm/demos/deb2rpm/util/logger.py | 0 .../demos/deb2rpm/util/yaml_handler.py | 0 tools/deb2rpm/doc/classes.drawio.png | Bin 0 -> 32892 bytes tools/deb2rpm/doc/db_process.drawio.png | Bin 0 -> 45655 bytes tools/deb2rpm/doc/deb2rpm_doc.md | 268 +++++++++++ tools/deb2rpm/doc/design.md | 26 + tools/deb2rpm/doc/examples.md | 137 ++++++ tools/deb2rpm/docs/assets/servers.png | Bin 0 -> 33634 bytes tools/deb2rpm/docs/assets/user-case.png | Bin 0 -> 54727 bytes .../docs/design_docs/deb2rpm-design.md | 79 +++ tools/deb2rpm/initialize.sh | 69 +++ tools/deb2rpm/scripts/deb_parser.py | 166 +++++++ tools/deb2rpm/scripts/get_dependency.py | 139 ++++++ tools/deb2rpm/scripts/get_files.py | 453 ++++++++++++++++++ tools/deb2rpm/scripts/get_repos.py | 57 +++ tools/deb2rpm/scripts/init_db.py | 138 ++++++ tools/deb2rpm/scripts/json2spec.py | 41 ++ tools/deb2rpm/scripts/recursive_depends.py | 252 ++++++++++ tools/deb2rpm/scripts/spec.py | 314 ++++++++++++ tools/deb2rpm/scripts/sqlite_database.py | 210 ++++++++ 35 files changed, 3104 insertions(+) create mode 100644 tools/deb2rpm/.gitignore create mode 100644 tools/deb2rpm/README.en.md create mode 100644 tools/deb2rpm/README.md create mode 100644 tools/deb2rpm/config/deb2rpm_mapping.json create mode 100644 tools/deb2rpm/config/resources.json create mode 100644 tools/deb2rpm/deb2rpm.py create mode 100644 tools/deb2rpm/deb2rpm_manager.py create mode 100644 tools/deb2rpm/demos/deb2rpm/conf/config.yaml create mode 100644 tools/deb2rpm/demos/deb2rpm/conf/logger.conf create mode 100644 tools/deb2rpm/demos/deb2rpm/conf/pkg_name_maaping.json create mode 100644 tools/deb2rpm/demos/deb2rpm/deb2rpm.py create mode 100644 tools/deb2rpm/demos/deb2rpm/src/data.py create mode 100644 tools/deb2rpm/demos/deb2rpm/src/package.py create mode 100644 tools/deb2rpm/demos/deb2rpm/util/command.py create mode 100644 tools/deb2rpm/demos/deb2rpm/util/deb_helper.py create mode 100644 tools/deb2rpm/demos/deb2rpm/util/logger.py create mode 100644 tools/deb2rpm/demos/deb2rpm/util/yaml_handler.py create mode 100755 tools/deb2rpm/doc/classes.drawio.png create mode 100644 tools/deb2rpm/doc/db_process.drawio.png create mode 100644 tools/deb2rpm/doc/deb2rpm_doc.md create mode 100755 tools/deb2rpm/doc/design.md create mode 100644 tools/deb2rpm/doc/examples.md create mode 100644 tools/deb2rpm/docs/assets/servers.png create mode 100644 tools/deb2rpm/docs/assets/user-case.png create mode 100644 tools/deb2rpm/docs/design_docs/deb2rpm-design.md create mode 100755 tools/deb2rpm/initialize.sh create mode 100644 tools/deb2rpm/scripts/deb_parser.py create mode 100644 tools/deb2rpm/scripts/get_dependency.py create mode 100644 tools/deb2rpm/scripts/get_files.py create mode 100644 tools/deb2rpm/scripts/get_repos.py create mode 100644 tools/deb2rpm/scripts/init_db.py create mode 100644 tools/deb2rpm/scripts/json2spec.py create mode 100644 tools/deb2rpm/scripts/recursive_depends.py create mode 100644 tools/deb2rpm/scripts/spec.py create mode 100644 tools/deb2rpm/scripts/sqlite_database.py diff --git a/tools/deb2rpm/.gitignore b/tools/deb2rpm/.gitignore new file mode 100644 index 0000000..368cfcf --- /dev/null +++ b/tools/deb2rpm/.gitignore @@ -0,0 +1,7 @@ +# IDE +.idea + +database/deb2rpm.db +__pycache__ + + diff --git a/tools/deb2rpm/README.en.md b/tools/deb2rpm/README.en.md new file mode 100644 index 0000000..13cf1e0 --- /dev/null +++ b/tools/deb2rpm/README.en.md @@ -0,0 +1,36 @@ +# deb2rpm + +#### Description +{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**} + +#### Software Architecture +Software architecture description + +#### Installation + +1. xxxx +2. xxxx +3. xxxx + +#### Instructions + +1. xxxx +2. xxxx +3. xxxx + +#### Contribution + +1. Fork the repository +2. Create Feat_xxx branch +3. Commit your code +4. Create Pull Request + + +#### Gitee Feature + +1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md +2. Gitee blog [blog.gitee.com](https://blog.gitee.com) +3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) +4. The most valuable open source project [GVP](https://gitee.com/gvp) +5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) +6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/tools/deb2rpm/README.md b/tools/deb2rpm/README.md new file mode 100644 index 0000000..feb14e5 --- /dev/null +++ b/tools/deb2rpm/README.md @@ -0,0 +1,53 @@ +# deb2rpm + +#### 介绍 +本项目目标为openEuler引入Debian的软件包,将deb包转化为openEuler上可用的rpm包及srpm源码包。 + +#### 软件架构 +- 主要支持openEuler-22.03_LTS +- deb包源支持Ubuntu各版本,如jammy(20)、bionic(18)等 + +#### 安装教程 + +1. 在家目录下`git clone`本项目 +2. 执行`./initialize.sh`进行初始化(数据库初始化可能需要较长时间) + + +#### 使用说明 + +执行`python3 deb2rpm.py -h`查看帮助信息如下: +``` +usage: deb2rpm.py [-h] [-d DEP_JSON] [-D DEEP_DEP_JSON] [-t BUILD_TYPE] [-s] [-k] [-a] [-b] target + +Build the target from deb to rpm + +positional arguments: + target The target package + +options: + -h, --help show this help message and exit + -d DEP_JSON, --dep-json DEP_JSON + Specify the path of json file with target dependency + -D DEEP_DEP_JSON, --deep-dep-json DEEP_DEP_JSON +<<<<<<< HEAD +======= + Specify the path of json file with all recursive dependency of target +>>>>>>> c2ad216 (Update docs) + -t BUILD_TYPE, --build-type BUILD_TYPE + The type of building: source_and_binary(default) | source | binary + -s, --show-dep-only Only show the dependency infomation but not build + -k, --keep-all-rpms Use if you want to keep all the rpms, else only keep rpms of target + -a, --auto-build Auto build the target recursively + -b, --build-dep Install all the exist build-depends +``` +对以上参数的具体说明如下: +- target:需要转换的deb源码包名,比如`zip`、`nginx`等 +- `-d`、`--dep-json`:指定target的依赖关系json文件路径,允许相对路径和绝对路径,如果不指定则不保存对应的json文件 +- `-D`、 `--deep-dep-json`:指定target的所有递归全量依赖关系json文件路径,允许相对路径和绝对路径,如果不指定则不保存对应的json文件 +- `-t`、`--build-type`:指定构建类型,可选值为`source_and_binary`、`source`、`binary`,默认为`source_and_binary`,分别对应源码包和二进制包同时构建、只构建源码包、只构建二进制包 +- `-s`、`--show-dep-only`:仅展示target的依赖关系,不进行构建 +- `-k`、`--keep-all-rpms`:构建完成后保留所有构建出的rpm包,否则只保留target的rpm包 +- `-a`、`--auto-build`:自动构建target的所有递归依赖关系 +- `-b`、`--build-dep`:安装target的所有构建依赖 + +具体例子可参考[doc/examples.md](./doc/examples.md) diff --git a/tools/deb2rpm/config/deb2rpm_mapping.json b/tools/deb2rpm/config/deb2rpm_mapping.json new file mode 100644 index 0000000..06169ea --- /dev/null +++ b/tools/deb2rpm/config/deb2rpm_mapping.json @@ -0,0 +1,76 @@ +{ + "debhelper": "debhelper", + "debhelper-compat": "debhelper", + "dpkg": "dpkg", + "dpkg-dev": "dpkg-devel", + "dpkg-perl": "dpkg-perl", + "po-debconf": "po-debconf", + "dh-autoreconf": "dh-autoreconf", + "pkg-config": "pkgconf", + "autoconf": "autoconf", + "automake": "automake", + "libc6": "glibc", + "libc6-dev": "glibc-devel", + "libbz2-1.0": "bzip2", + "libbz2-dev": "bzip2-devel", + "libexpat-dev": "expat-devel", + "libgd": "gd", + "libgd-dev": "gd-devel", + "libgeoip": "geoip", + "libgeoip-dev": "geoip-devel", + "libhiredis": "hiredis", + "libhiredis-dev": "hiredis-devel", + "libmaxminddb-dev": "libmaxminddb-devel", + "libmhash": "mhash", + "libmhash-dev": "mhash-devel", + "libpam0g": "pam", + "libpam0g-dev": "pam-devel", + "libpcre3": "pcre", + "libpcre3-dev": "pcre-devel", + "libperl": "perl", + "libperl-dev": "perl-devel", + "libssl": "openssl", + "libssl-dev": "openssl-devel", + "libxslt1": "libxslt", + "libxslt1-dev": "libxslt-devel", + "quilt": "quilt", + "zlib1g-dev": "zlib-devel", + "bison": "bison", + "libevent": "libevent", + "libevent-dev": "libevent-devel", + "libncurses5": "ncurses-libs", + "libncurses6": "ncurses-libs", + "libtinfo6": "ncurses-libs", + "libncursesw6": "ncurses-libs", + "ncurses-bin": "ncurses", + "libncurses-dev": "ncurses-devel", + "libncurses5-dev": "ncurses-devel", + "libncurses6-dev": "ncurses-devel", + "libutempter0": "libutempter", + "libutempter-dev": "libutempter-devel", + "libglib2.0": "glib2", + "libglib2.0-0": "glib2", + "libglib2.0-dev": "glib2-devel", + "texinfo": "texinfo", + "libtool": "libtool", + "libgpm": "gpm", + "libgpm-dev": "gpm-devel", + "libmd-dev": "libmd-devel", + "m4": "m4", + "perl": "perl", + "g++-multilib": "libstdc++-devel", + "gettext": "gettext", + "libacl1": "libacl", + "libacl1-dev": "libacl-devel", + "libselinux1": "libselinux", + "libselinux1-dev": "libselinux-devel", + "libattr1": "libattr", + "libattr1-dev": "libattr-devel", + "libjemalloc-dev": "jemalloc-devel", + "liblua5.1-dev": "lua-devel", + "liblzf-dev": "liblzf-devel", + "libsystemd-dev": "systemd-devel", + "procps": "procps-ng", + "tcl": "tcl", + "tcl-tls": "tcltls" +} diff --git a/tools/deb2rpm/config/resources.json b/tools/deb2rpm/config/resources.json new file mode 100644 index 0000000..af06a42 --- /dev/null +++ b/tools/deb2rpm/config/resources.json @@ -0,0 +1,79 @@ +{ + "refresh": false, + "repo_url": "https://repo.huaweicloud.com/ubuntu", + "main": { + "enable": true, + "Sources": { + "main": "/home/young/Source20/jammy/Sources-main", + "multiverse": "/home/young/Source20/jammy/Sources-multiverse", + "restricted": "/home/young/Source20/jammy/Sources-restricted", + "universe": "/home/young/Source20/jammy/Sources-universe" + }, + "Packages": { + "main": "/home/young/Package20/jammy/Packages-main", + "multiverse": "/home/young/Package20/jammy/Packages-multiverse", + "restricted": "/home/young/Package20/jammy/Packages-restricted", + "universe": "/home/young/Package20/jammy/Packages-universe" + } + }, + "updates": { + "enable": true, + "Sources": { + "main": "/home/young/Source20/jammy-updates/Sources-main", + "multiverse": "/home/young/Source20/jammy-updates/Sources-multiverse", + "restricted": "/home/young/Source20/jammy-updates/Sources-restricted", + "universe": "/home/young/Source20/jammy-updates/Sources-universe" + }, + "Packages": { + "main": "/home/young/Package20/jammy-updates/Packages-main", + "multiverse": "/home/young/Package20/jammy-updates/Packages-multiverse", + "restricted": "/home/young/Package20/jammy-updates/Packages-restricted", + "universe": "/home/young/Package20/jammy-updates/Packages-universe" + } + }, + "backports": { + "enable": true, + "Sources": { + "main": "/home/young/Source20/jammy-backports/Sources-main", + "multiverse": "/home/young/Source20/jammy-backports/Sources-multiverse", + "restricted": "/home/young/Source20/jammy-backports/Sources-restricted", + "universe": "/home/young/Source20/jammy-backports/Sources-universe" + }, + "Packages": { + "main": "/home/young/Package20/jammy-backports/Packages-main", + "multiverse": "/home/young/Package20/jammy-backports/Packages-multiverse", + "restricted": "/home/young/Package20/jammy-backports/Packages-restricted", + "universe": "/home/young/Package20/jammy-backports/Packages-universe" + } + }, + "security": { + "enable": true, + "Sources": { + "main": "/home/young/Source20/jammy-security/Sources-main", + "multiverse": "/home/young/Source20/jammy-security/Sources-multiverse", + "restricted": "/home/young/Source20/jammy-security/Sources-restricted", + "universe": "/home/young/Source20/jammy-security/Sources-universe" + }, + "Packages": { + "main": "/home/young/Package20/jammy-security/Packages-main", + "multiverse": "/home/young/Package20/jammy-security/Packages-multiverse", + "restricted": "/home/young/Package20/jammy-security/Packages-restricted", + "universe": "/home/young/Package20/jammy-security/Packages-universe" + } + }, + "proposed": { + "enable": true, + "Sources": { + "main": "/home/young/Source20/jammy-proposed/Sources-main", + "multiverse": "/home/young/Source20/jammy-proposed/Sources-multiverse", + "restricted": "/home/young/Source20/jammy-proposed/Sources-restricted", + "universe": "/home/young/Source20/jammy-proposed/Sources-universe" + }, + "Packages": { + "main": "/home/young/Package20/jammy-proposed/Packages-main", + "multiverse": "/home/young/Package20/jammy-proposed/Packages-multiverse", + "restricted": "/home/young/Package20/jammy-proposed/Packages-restricted", + "universe": "/home/young/Package20/jammy-proposed/Packages-universe" + } + } +} diff --git a/tools/deb2rpm/deb2rpm.py b/tools/deb2rpm/deb2rpm.py new file mode 100644 index 0000000..f20d67c --- /dev/null +++ b/tools/deb2rpm/deb2rpm.py @@ -0,0 +1,264 @@ +import argparse +import subprocess +import os +import re +import sys +import logging +import platform + +from typing import List + +from deb2rpm_manager import DRManager + +current_dir = os.path.dirname(os.path.abspath(__file__)) + +project_root = os.path.abspath(os.path.join(current_dir, "..")) +sys.path.append(project_root) + +repos = ['main', 'updates', 'backports', 'security', 'proposed'] + +logging.basicConfig(level=logging.INFO, + format='%(name)s - %(levelname)s - %(message)s') + +logger = logging.getLogger(__name__) + +home = os.getenv("HOME") +machine = platform.machine() + +type2param = { + "source_and_binary": "-ba", + "source": "-bs", + "binary": "-bb" +} + +def get_abs_path(path: str) -> str: + if os.path.isabs(path): + abs_path = path + else: + current_directory = os.getcwd() + abs_path = os.path.abspath(os.path.join(current_directory, path)) + + abs_path = os.path.expanduser(abs_path) + abs_path = os.path.normpath(abs_path) + + return abs_path + +def get_version(deb_version: str) -> str: + """ + 获取Epoch, Version, Release信息 + @param deb_version deb版本字符串 + """ + version_string = deb_version + epoch_pattern = r'^(\d+):' + release_pattern = r'(.*?)-(.*)' + + match_epoch = re.match(epoch_pattern, deb_version) + if match_epoch: + epoch = match_epoch.group(1) + version_string = version_string[len(epoch)+1:] + match_release = re.match(release_pattern, version_string) + if match_release: + release = match_release.group(2).strip() + version_string = version_string[:-len(release)-1] + return version_string + +def format_and_print_strings(strings, strings_per_row=3, string_width=20): + for i, string in enumerate(strings): + formatted_string = string.center(string_width) + + print(formatted_string, end=" ") + + if i < len(strings) - 1 and (i + 1) % strings_per_row != 0: + continue + else: + print() + +def check_packages_installed(packages: List) -> List: + uninstalled_list = [] + for package in packages: + try: + res = subprocess.run(['dnf', 'info', package], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if res.returncode != 0: + uninstalled_list.append(package) + except Exception as e: + uninstalled_list.append(package) + + return uninstalled_list + +class DRClient: + def __init__(self, target: str, show_dep_only: bool, keep_all_rpms: bool, get_files_by_debs: bool, build_dep=False, auto_build=False, client_type="source_and_binary", install_list=None, spec_dep_json="", deep_dep_json=""): + self.target = target + self.keep_all_rpms = keep_all_rpms + self.manager = DRManager(target) + self.get_repo() + single_deps = self.manager.get_single_deps(self.chosen_repo, spec_dep_json) + if show_dep_only: + self.show_deps(single_deps) + sys.exit() + build_dep_list = [list(bd.split())[0] for bd in single_deps['Build-Depends'].keys() if single_deps['Build-Depends'][bd]] + if build_dep: + self.install_all_exists(build_dep_list) + sys.exit() + + not_installed = check_packages_installed(build_dep_list) + if len(not_installed) != 0: + logger.critical(f"以下依赖未安装: {not_installed}") + sys.exit() + if auto_build: + all_deps = self.manager.get_all_deps(self.chosen_repo, deep_dep_json) + self.install_all_exists(all_deps['exists']) + self.build_source(all_deps['source_order']) + self.genarate_spec(spec_dep_json, get_files_by_debs) + res = os.system(f"rpmbuild {type2param[client_type]} {self.spec_path}") + if res != 0: + print(f"构建{self.target}失败!") + sys.exit() + self.all_rpms = self.manager.get_all_rpms(self.chosen_repo, self.version) + logger.info(f"完成{self.target}的 {client_type} 构建!") + if client_type == "binary" and install_list != None and len(install_list) > 0: + logger.info("安装依赖: " + ' '.join(install_list)) + for i in install_list: + rpm_path = self.all_rpms[i] + command = f"sudo rpm -i {rpm_path}" + logger.info(f"安装{i}: 执行命令 {command}") + os.system(command) + + def get_repo(self): + all_repo_and_versions = self.manager.get_all_targets() + + if len(all_repo_and_versions) == 0: + print(f"未找到{self.target}!") + sys.exit() + + print(f"搜索到以下版本的{self.target}:") + cnt = 1 + for key, value in all_repo_and_versions.items(): + print(f"{cnt}: {key.center(10)} {value}") + cnt += 1 + + print(f"请选择所需要的版本,输入对应的标号(1~{cnt-1}):") + while True: + try: + num = int(input()) + if num >= 1 and num <= cnt - 1: + self.chosen_repo = list(all_repo_and_versions.keys())[num-1] + break + else: + print(f"输入越界!请重新输入对应标号(1~{cnt-1}):") + + except ValueError: + print(f"输入不合法!请重新输入对应标号(1~{cnt-1}):") + + self.version = get_version(all_repo_and_versions[self.chosen_repo]) + + def show_deps(self, single_deps): + print(f"{'#'*20}{(self.target + '构建依赖').center(20)}{'#'*27}") + print("Build-Depends:") + for i, whether_rpm in single_deps['Build-Depends'].items(): + if whether_rpm: + print(f" [RPM] {i}".center(60)) + else: + print(f" [DEB] {i}".center(60)) + print('#'*71) + for package in single_deps['Packages'].keys(): + print(f"{'#'*20}{(package + '运行依赖').center(20)}{'#'*27}") + for dep_type in single_deps['Packages'][package].keys(): + print(dep_type + ":") + for i, whether_rpm in single_deps['Packages'][package][dep_type].items(): + if whether_rpm: + print(f" [RPM] {i}".center(60)) + else: + print(f" [DEB] {i}".center(60)) + print('#'*71) + + + def install_all_exists(self, packages: List): + """ + 安装所有可直接安装或引入的依赖 + @param packages 所有可直接安装或引入的依赖列表 + """ + if len(packages) == 0: + print("没有可直接安装或引入的依赖!") + return + + command = 'sudo dnf install --nogpgcheck ' + ' '.join(packages) + logger.info("正在安装所有可直接引入或安装的依赖") + logger.info(f"执行命令: {command}") + os.system(command) + + def build_source(self, sources: List, force: bool): + """ + 编译所有需要额外编译的源码包 + @param sources 所有需要额外编译的源码包列表 + @param force 是否强制编译 + """ + if force: + return + for s in sources: + s_client = DRClient(s, show_dep_only=False, keep_all_rpms=self.keep_all_rpms, auto_build=True, client_type="binary", install_list=self.all_deps['source_and_package'][s]) + if not self.keep_all_rpms: + for rpm_path in s_client.all_rpms.values(): + os.system(f"rm -f {rpm_path}") + + def genarate_spec(self, json_path, get_files_by_debs): + print('#'*71) + logger.info(f"开始生成{self.target}的spec文件!") + self.manager.gen_spec(self.chosen_repo, get_files_by_debs) + self.spec_path = self.manager.add_dep2spec(json_path) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Build the target from deb to rpm") + # parser.add_argument("-n", "--newest", action="store_true") + group = parser.add_mutually_exclusive_group() + parser.add_argument("target", help="The target package") + parser.add_argument("-d", "--dep-json", help="Specify the path of json file with target dependency") + parser.add_argument("-D", "--deep-dep-json", help="Specify the path of json file with all recursive dependency of target") + parser.add_argument("-t", "--build-type", help="The type of building: source_and_binary(default) | source | binary") + group.add_argument("-s", "--show-dep-only", action="store_true", help="Only show the dependency infomation but not build") + parser.add_argument("-k", "--keep-all-rpms", action="store_true", help="Use if you want to keep all the rpms, else only keep rpms of target") + group.add_argument("-a", "--auto-build", action="store_true", help="Auto build the target recursively") + group.add_argument("-b", "--build-dep", action="store_true", help="Install all the exist build-depends") + + parser.add_argument("-g", "--get-files-by-debs", action="store_true", help="Fill the %%files in spec by downloading debs, deleted afterwards and generating quicker") + + args = parser.parse_args() + + show_dep_only = False + keep_all_rpms = False + auto_build = False + build_dep = False + get_files_by_debs = False + spec_dep_json = "" + deep_dep_json = "" + t = "source_and_binary" + + if args.show_dep_only: + show_dep_only = True + if args.keep_all_rpms: + keep_all_rpms = True + if args.auto_build: + auto_build = True + if args.build_dep: + build_dep = True + if args.get_files_by_debs: + get_files_by_debs = True + if args.dep_json: + spec_dep_json = get_abs_path(args.dep_json) + if args.deep_dep_json: + deep_dep_json = args.deep_dep_json + if args.build_type: + if args.build_type in type2param.keys(): + t = args.build_type + else: + print("Unsupported Type!") + sys.exit() + + client = DRClient(args.target, show_dep_only=show_dep_only, keep_all_rpms=keep_all_rpms, get_files_by_debs=get_files_by_debs, build_dep=build_dep, auto_build=auto_build, client_type=t, spec_dep_json=spec_dep_json, deep_dep_json=deep_dep_json) +<<<<<<< HEAD + +======= + # SpecGenarator(chosen_repo, args.target) + # dj = DepJson(chosen_repo, args.target) + +>>>>>>> b3ad248 (Add the option of downloading debs to get files) diff --git a/tools/deb2rpm/deb2rpm_manager.py b/tools/deb2rpm/deb2rpm_manager.py new file mode 100644 index 0000000..356a960 --- /dev/null +++ b/tools/deb2rpm/deb2rpm_manager.py @@ -0,0 +1,150 @@ +import os +import sys +import json +import logging +import platform + +from typing import Dict, List + +current_dir = os.path.dirname(os.path.abspath(__file__)) + +project_root = os.path.abspath(os.path.join(current_dir, "..")) +sys.path.append(project_root) + +from scripts.init_db import InitDB +from scripts.spec import SpecGenarator +from scripts.get_dependency import DepJson +from scripts.recursive_depends import DeepDep +from scripts.json2spec import Json2Spec + +home = os.getenv("HOME") +machine = platform.machine() +mapping_path = f"{home}/deb2rpm/config/deb2rpm_mapping.json" +logger = logging.getLogger(__name__) + +repos = ['main', 'updates', 'backports', 'security', 'proposed'] + +class DRManager: + def __init__(self, target: str): + """ + 初始化Deb2Rpm Manager + @param target 目标包名 + @param save_dep_json 目标包依赖文件路径,默认为SPECS目录 + """ + self.target = target + with open(mapping_path, 'r') as file: + self.mapping = json.load(file) + + def get_all_targets(self) -> Dict: + """ + 从各个repo源中寻找目标包 + @return 返回各个repo源对应的目标包版本 + """ + db = InitDB() + all_repo_and_versions = {} + for repo in repos: + info = db.get_source_info(repo, self.target) + if info != None: + all_repo_and_versions[repo] = info[1] + + return all_repo_and_versions + + def get_single_deps(self, repo: str, json_path="") -> Dict: + dj = DepJson(repo, self.target, json_path=json_path) + data = {} + data["Build-Depends"] = {} + for bd in dj.dic["Build-Depends"]: + bd_name = list(bd.split())[0] + if bd_name in self.mapping.values(): + data["Build-Depends"][bd] = True + else: + data["Build-Depends"][bd] = False + + data["Packages"] = {} + for package in dj.dic["Packages"].keys(): + data["Packages"][package] = {} + for dep_type in dj.dic["Packages"][package].keys(): + data["Packages"][package][dep_type] = {} + for d in dj.dic["Packages"][package][dep_type]: + d_name = list(d.split())[0] + if d_name in self.mapping.values(): + data["Packages"][package][dep_type][d] = True + else: + data["Packages"][package][dep_type][d] = False + + if json_path != "": + dj.write_into_json() + + return data + + def get_all_deps(self, repo: str, json_path="") -> Dict: + """ + 获取repo中对应target的全量依赖 + @param repo 选择的repo源 + @param json_path 全量依赖json文件的保存路径,默认为不保存 + @return 返回所有依赖项 + [目标包依赖,可直接安装依赖,额外需要的依赖,额外需要编译的源码包] + """ + return_values = {} + # dj = DepJson(repo, self.target) + dd = DeepDep(repo, self.target, json_path=json_path) + # return_values["single_dep"] = dj.dic + return_values["exists"] = dd.exists + return_values["package_order"] = dd.package_order + return_values["source_order"] = dd.source_order + return_values["source_and_package"] = dd.source_and_package + + dd.write_into_json() + + return return_values + + def gen_spec(self, repo: str, download_debs: bool) -> str: + """ + 生成repo中对应target的spec文件 + @param repo 选择的repo源 + @return 返回spec文件保存路径 + """ + sg = SpecGenarator(repo, self.target, download_debs) + self.repo = repo + self.spec_file_path = sg.spec_file_path + return sg.spec_file_path + + def add_dep2spec(self, json_path="") -> str: + """ + 将依赖项填入生成的缺少依赖的spec文件 + 如果未指定依赖json文件保存路径,则在SPECS目录中生成后删除 + @return 返回完成的spec文件路径 + """ + remove_json = False + if json_path == "": + remove_json = True + specs_path = os.path.dirname(self.spec_file_path) + json_path = os.path.join(specs_path, f"{self.target}.json") + + dj = DepJson(self.repo, self.target, json_path=json_path) + dj.write_into_json() + + Json2Spec(self.spec_file_path, json_path) + logger.info(f"完成目标spec文件: {self.spec_file_path}") + + if remove_json: + os.remove(json_path) + else: + logger.info(f"生成依赖json文件: {json_path}") + + return self.spec_file_path + + def get_all_rpms(self, repo: str, version: str) -> Dict: + db = InitDB() + source_info = db.get_source_info(repo, self.target) + all_binary = source_info[4].strip().split() + binary2rpm = {} + + for binary in all_binary: + if binary == self.target: + rpm_name = f"{self.target}-{version}-1.{machine}.rpm" + else: + rpm_name = f"{self.target}-{binary}-{version}-1.{machine}.rpm" + binary2rpm[binary] = f"{home}/rpmbuild/RPMS/{machine}/{rpm_name}" + + return binary2rpm diff --git a/tools/deb2rpm/demos/deb2rpm/conf/config.yaml b/tools/deb2rpm/demos/deb2rpm/conf/config.yaml new file mode 100644 index 0000000..e69de29 diff --git a/tools/deb2rpm/demos/deb2rpm/conf/logger.conf b/tools/deb2rpm/demos/deb2rpm/conf/logger.conf new file mode 100644 index 0000000..e69de29 diff --git a/tools/deb2rpm/demos/deb2rpm/conf/pkg_name_maaping.json b/tools/deb2rpm/demos/deb2rpm/conf/pkg_name_maaping.json new file mode 100644 index 0000000..e69de29 diff --git a/tools/deb2rpm/demos/deb2rpm/deb2rpm.py b/tools/deb2rpm/demos/deb2rpm/deb2rpm.py new file mode 100644 index 0000000..57ec1db --- /dev/null +++ b/tools/deb2rpm/demos/deb2rpm/deb2rpm.py @@ -0,0 +1,58 @@ +import logging +import argparse +import os.path +import queue + +from src.data import Data +from src.package import Deb, Depend +from util.logger import Logger +from util.yaml_handler import YamlHandler +from util.deb_helper import Debhelper + +Logger.init_logger() +logger = logging.getLogger('deb2rpm') + + +def do_args(): + parser = argparse.ArgumentParser() + + parser.add_argument("-c", "--config_path", default="./conf/config.yaml", help="config yaml path") + parser.add_argument("pkg", type=str, help="The Debina Package Name") + + return parser + + +def normalize_deb_depend(deb: Deb, data: Data): + for depend in deb.depend_list: + version = depend.version + if depend.pkg.endswith(':any'): + depend.set_pkg(depend.pkg[:-4]) + if not version: + continue + if ':' in version: + version = version.split(':')[-1] + num_depend = version.count('-') + version_deb = data.get_deb_bersion_by_package(deb.Package) + num_deb = version_deb.count('-') + if num_deb and num_deb == num_depend: + version = version.split('-')[0] + if depend.flag in ('<', '<<'): + depend.set_flag('<=') + depend.set_depend_version(version) + + +def pkg_name_mapping(pkg: str, depend_pkg: str, depend: Depend, data: Data): + pass + + +def breadth_first_search_package(pkg_list: list, data: Data): + pass + + +def main(): + parser = do_args() + pass + + +if __name__ == "__main__": + main() diff --git a/tools/deb2rpm/demos/deb2rpm/src/data.py b/tools/deb2rpm/demos/deb2rpm/src/data.py new file mode 100644 index 0000000..10445dc --- /dev/null +++ b/tools/deb2rpm/demos/deb2rpm/src/data.py @@ -0,0 +1,32 @@ +class Data: + + def __init__(self, config: dict): + self.config = config + self.deb_db = None + self.src_deb_db = None + self.rpm_db = None + self.pkg_name_mapping_db = None + self.load_database() + + def load_database(self): + db_path = os.path.join(self.config['workdir'], 'database') + deb_primary, rpm_primary = self.download_database(db_path) + + def download_database(self, db_path): + pass + + def normalize_deb_json_database(self, file_list: list): + pass + + def normalize_source_deb_database(self, dic: dict): + pass + + def normalize_deb_provides_database(self, dic: dict): + pass + + def normalize_rpm_sqlite_database(self, file_list: list): + pass + + def normalize_pkg_name_mapping_database(self, file_list: list): + pass + diff --git a/tools/deb2rpm/demos/deb2rpm/src/package.py b/tools/deb2rpm/demos/deb2rpm/src/package.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/deb2rpm/demos/deb2rpm/util/command.py b/tools/deb2rpm/demos/deb2rpm/util/command.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/deb2rpm/demos/deb2rpm/util/deb_helper.py b/tools/deb2rpm/demos/deb2rpm/util/deb_helper.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/deb2rpm/demos/deb2rpm/util/logger.py b/tools/deb2rpm/demos/deb2rpm/util/logger.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/deb2rpm/demos/deb2rpm/util/yaml_handler.py b/tools/deb2rpm/demos/deb2rpm/util/yaml_handler.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/deb2rpm/doc/classes.drawio.png b/tools/deb2rpm/doc/classes.drawio.png new file mode 100755 index 0000000000000000000000000000000000000000..8d151b94850be9b45282cde1bf24637e20c996f9 GIT binary patch literal 32892 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 QQ9cAdhL=GU5WMjJKgeXEa{vGU literal 0 HcmV?d00001 diff --git a/tools/deb2rpm/doc/db_process.drawio.png b/tools/deb2rpm/doc/db_process.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..ac05480257acd0aa50445e5653f7d14b72821dec GIT binary patch literal 45655 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 literal 0 HcmV?d00001 diff --git a/tools/deb2rpm/doc/deb2rpm_doc.md b/tools/deb2rpm/doc/deb2rpm_doc.md new file mode 100644 index 0000000..eb59b32 --- /dev/null +++ b/tools/deb2rpm/doc/deb2rpm_doc.md @@ -0,0 +1,268 @@ +# deb2rpm +[TOC] +## 1. 需求 +1. 功能需求:将deb软件包转换为能够在openEuler 22.03-LTS上运行的rpm软件包以及src.rpm源码包 +2. 兼容需求:转换出的rpm软件包和包含spec文件的src.rpm源码包,要能在openEuler及openEuler的衍生发行版上正常构建和运行 +3. 自动化需求:转换工具能够通用性自动化地转换大部分deb软件包 + +## 2. 需要解决的困难 +1. deb软件包和rpm软件包的文件结构和部分语法不同,需要进行内容的映射,部分独有的内容需要特殊处理 +2. deb软件包和rpm软件包使用两套包管理器,其依赖对象不同,需要进行依赖的映射 + +## 3. 方案总体设计 +### 3.1 spec模板 +rpm包的关键内容就是spec文件,spec文件的基本格式如下: +```spec +################################################################## +# 1. 元数据信息 +################################################################## +Name: +Version: +Release: +Summary: +License: +URL: +Source0: +... +Patch0: +... + +%description + +################################################################## +# 2. 依赖信息 +################################################################## +BuildRequires: +Requires: +... + +################################################################## +# 3. 安装信息 +################################################################## +%prep + +%build + +%install + +################################################################## +# 4. 子包信息 +################################################################## +%package -n +Summary: +Requires: + +%description -n + +################################################################## +# 5. 文件信息 +################################################################## +%files +... + +%files -n +... + +################################################################## +# 6. changelog +################################################################## +%changelog +... +``` + +### 3.2 类图 +要从不同文件的不同位置提取出以上构建spec文件的内容,本工具中包含的类的总体设计如下图所示: +![classes](./classes.drawio.png) +上图中各个类的基本功能如下: +- `DscParser`:解析dsc文件,获取主软件包的元数据信息 +- `ControlParser`:解析control文件,获取子包的元数据信息 +- `LicenseParser`:解析license文件,获取license信息 +- `DebFiles`:获取文件信息 +- `Spec`:将获取到的deb信息转化为spec文件的格式 +- `BaseDB`: 调用sqlite数据库的接口 +- `SourceDB`:从Source文件(如[https://repo.huaweicloud.com/ubuntu/dists/jammy/main/source/Sources.gz](https://repo.huaweicloud.com/ubuntu/dists/jammy/main/source/Sources.gz))构建数据库 +- `PackageDB`:从Package文件(如[https://repo.huaweicloud.com/ubuntu/dists/jammy/main/binary-amd64/Packages.gz](https://repo.huaweicloud.com/ubuntu/dists/jammy/main/binary-amd64/Packages.gz))构建数据库 +- `InitDB`: 获取源文件及依赖的数据库 + +类与spec文件内容的对应关系如下: +| 类 | spec | +| :---: | :---: | +| InitDB | 依赖相关 | +| DscParser | Name, Epoch, Version, Release, URL, Source | +| ControlParser | Summary, %description, 子包名 | +| LicenseParser | License | +| DebParser | Patch | +| DebFiles | %build, %files | +| SpecGenarator | %prep, %install, %changelog | + +### 3.3 整体流程 +1. 初始化或更新数据库信息 +2. 在`SOURCES`目录下创建与软件包同名的文件夹,并根据数据库中的源文件信息获取deb源码包,并解压得到源码文件夹 +3. `DscParser`解析dsc文件获取主软件包的元数据信息 +4. `ControlParser`解析control文件获取子包的元数据信息 +5. `LicenseParser`解析license文件获取license信息 +6. 从debian/patches文件夹中获取补丁文件并复制到步骤二创建的文件夹中 +7. 调用`get_files`脚本获取安装文件列表 +8. 将元上述信息转化为spec中对应的内容 +9. 生成spec文件 +10. 将Source和Patch文件复制到`SOURCES`中,并删除步骤二创建的文件夹 + +## 4. 具体实现 +下面按照spec文件的各个模块来介绍具体的实现方法。 +### 4.1 Source和依赖相关内容 +本部分是从Source文件和Package文件中的已有内容获取的,用户通过改写`resources.json`文件中的文件路径来设置使用的本地文件,该json文件模板如下: +```json +{ + "refresh": false, + "Sources": { + "main": "/path/to/main/source", + "multiverse": "/path/to/multiverse/source", + "restricted": "/path/to/restricted/source", + "universe": "/path/to/universe/source" + }, + "Packages": { + "main": "/path/to/main/package", + "multiverse": "/path/to/multiverse/package", + "restricted": "/path/to/restricted/package", + "universe": "/path/to/universe/package" + } +} +``` +为了减少重复的读写并方便做设置修改,数据库中除了将以上八个文件分别对应到八张表,还另设一张表用于存储文件路径信息。当json文件中某个文件的路径信息被修改了后,再次运行程序前会将对应的表更新为新文件,数据库初始化部分的流程图如下: +![db_process](./db_process.drawio.png) +数据库存储内容如下: +- path表: source和package文件路径信息 +- SourceMain, SourceMultiverse, SourceRestricted, SourceUniverse: +存储Source文件中后续需要的信息,包含源码包所在源的文件夹、构建依赖(*Build-Depends*)、所需要的源文件 +- PackageMain, PackageMultiverse, PackageRestricted, PackageUniverse: +存储Package文件中后续需要的信息,即各个软件包(包括子包)的依赖信息,包括`Provide`, `Pre-Depends`, `Depends`, `Recommends`, `Suggests`, `Breaks`, `Replaces` + +完成了数据库内容的获取,就可以将获取信息的接口`get_source_info`和`get_package_info`封装到`InitDB`类中了。 + +### 4.2 构建build目录 +获取到source信息后,工具就会在`SOURCES`目录下的临时文件夹中下载这些文件: +```python +source_name, build_requires, dir_path, source_files = get_source_info(package_name) + +web_repo = "https://repo.huaweicloud.com/ubuntu" +for file in source_files.split(): + os.system(f"wget {web_repo}/{dir_name}/{file}") +``` +随后执行命令`dpkg-source -x --no-check `来构建源码文件夹(build目录)。相比于`tar`的相关解压方式,该方案的优势在于: +- 无需考虑源码压缩包的多种压缩文件格式 +- 无需考虑解压路径的设置 +- 可进行任意数量的文件解压 +- 作为`apt source`的实际命令之一,`dpkg-source -x`会自动执行`debian/patches/series`中列出的补丁文件 + +### 4.3 Name, Version, URL, Sources +本部分内容由`DscParser`提取,由于dsc文件格式较为统一,因此这里使用了一个简单的信息提取函数: +```python +def get_line_info(line: str) -> List: + pattern = r'^(.*?):\s*(.*)' + match = re.search(pattern, line) + if match: + return match.group(1).strip(), match.group(2).strip() + return "", "" +``` +即如果一行字符串为 +``` +Version: 1.0.0 +``` +该函数就会返回`["Version", "1.0.0"]`,从而提取出需要的信息。 + +后续在`Spec`类中会对`Version`信息做进一步处理,因为deb中`Version`的格式为`[Epoch:]Version[-Release]`,分别对应spec中的三个字段,其中`Epoch`和`Release`是可选的,在本工具中`Epoch`默认为无,`Release`默认为`1` + +这里不从source文件而从dsc文件中获取这些信息的原因在于减少对于数据库的依赖,并且为将一个未收集信息的deb包转换为rpm包提供可能。 + +### 4.4 %package, BuildArch, Summary, %description +本部分内容由`ControlParser`获取,control文件格式类似于dsc文件,因此也可以采用`get_line_info`的方法。但`Summary`和`%description`信息有所不同,在control文件中,前者对应`Description`字段的第一行内容,后者对应该字段的剩余内容,而该字段后的内容要么就是下一个Package,要么就是文件结尾,因此可以如下处理: +```python +with open(path, 'r') as file: + package_name = "" + description = "" + + for line in file: + key, value = get_line_info(line) + if key == "Package": + description = False + package_name = value + self.package_data[package_name] = {} + if key == "Description": + self.package_data[package_name].update({"Summary": value}) + description = True + self.package_data[package_name]["description"] = "" + elif description: + self.package_data[package_name]["description"] += line +``` + +### 4.5 License +本部分由`LicenseParser`提取,根据deb包license文件的格式[规范](https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/),该文件总体的格式大致为: +``` +Files: * +Copyright: 1975-2010 Ulla Upstream +License: GPL-2+ + +Files: debian/* +Copyright: 2010 Daniela Debianizer +License: GPL-2+ + +Files: debian/patches/fancy-feature +Copyright: 2010 Daniela Debianizer +License: GPL-3+ + +Files: */*.1 +Copyright: 2010 Manuela Manpager +License: GPL-2+`` +``` +此处仅提取了`Files: *`下的`License`字段值填写spec文件中对应的`license`名。 +但也存在不满足这种规范的软件包,如`zip`。由于spec文件中`license`字段不可缺少,暂时将这种软件包的`license`填写为MIT + +### 4.6 Patch +本部分内容由`DebParser`获取。如前所述,补丁文件在构建build目录时会自动执行,因此在spec文件中并不需要手动执行补丁,但仍然需要列出补丁文件。事实上,deb包的补丁文件均在`debian/patches`文件夹下,因此只需获取该目录下所有的`.patch`和`.diff`文件即可。 + +### 4.7 %build, %files +本部分内容由`DebFiles`获取,由于deb包并不像rpm包这样显式地列出待安装目录,而是将各子包的待安装目录先安装在debian目录下。因此本工具会在`SOURCES`目录下创建的临时文件夹中完成deb包所有文件结构的构建,进而获取到各子包需要安装的文件。 + +尝试发现,在执行`fakeroot debian/rules binary`时,不同的包会执行不同的命令,但如果在中间插入无关的指令实际上并不会产生别的影响。因此,可以先确定一个通用的有序的全体命令集。 + +另一方面,`debian/rules`文件中可能含有部分命令的重载,如`override_dh_auto_install`,这时就需要将全体命令集中的`fakeroot dh_auto_install`替换为`fakeroot debian/rules dh_auto_isntall`。 +有时也存在两个替换的情况,即`fakeroot dh_install`替换为`fakeroot debian/rules dh_install-arch`和`fakeroot debian/rules dh_install-indep`。 + +通过对`debian/rules`文件的解析,就能获得文件信息的全部命令,也就是`%build`对应的内容。 + +### 4.8 %prep, %install +这两部分内容相对固定,由`SpecGenarator`类直接写入。 +`%prep`部分需要实现源码压缩包的解压,这里除了采用`dpkg-source`的解压方法,为了使得生成的spec文件更有通用性,本工具在此处采用tar命令的解压方法。需要在脚本中根据源码压缩包类型确定命令,具体如下: +| 压缩包类型 | 命令 | +| :---: | :---: | +| .tar.gz | tar -xzf | +| .tar.bz2 | tar -xjf | +| .tar.xz | tar -xJf | +```spec +%prep +rm -rf %{_builddir}/%{name}-%{version} +mkdir %{_builddir}/%{name}-%{version} +tar -xzf %{SOURCE0} -C %{_builddir}/%{name}-%{version} --strip-components=1 # 避免了解压文件夹名的不确定性 +tar -xJf %{SOURCE1} -C %{_builddir}/%{name}-%{version} + +cd %{_builddir}/%{name}-%{version} +%patch0 -p1 +... +``` +从而实现了rpm默认的build行为,即在`%{_builddir}`下创建名为`%{name}-%{version}`的目录,并在其中完成build +由于本阶段没有使用`setup`等默认的宏,因此`%build`的默认路径仍然是`%{_builddir}`, 因此需要在`%build`阶段最开始加上`cd %{name}-%{version}` + +`%install`部分需要实现的是将待安装文件全部复制到`%{buildroot}`目录下,由于需要实现的是文件树结构的复制,本工具采用的是`rsync`命令: +```spec +rm -rf %{buildroot}/* +rsync -a --ignore-existing --exclude='DEBIAN/' %{_builddir}/%{name}-%{version}/debian//* %{buildroot}/%{name}-%{version}-{release}. +... +``` + +### 4.9 %changelog +本部分内容暂时为统一格式,模板如下: +```spec +%changelog +* <星期> <月份> <日> <年份> OpenEuler %{version}-%{release} +- Build %{name} from deb to rpm +``` \ No newline at end of file diff --git a/tools/deb2rpm/doc/design.md b/tools/deb2rpm/doc/design.md new file mode 100755 index 0000000..6ebb3f6 --- /dev/null +++ b/tools/deb2rpm/doc/design.md @@ -0,0 +1,26 @@ +# deb2rpm +本部分完成了在Ubuntu环境下deb源码包生成对应spec文件,从而构建rpm包及rpm源码包。其中类的设计如下图所示: +![classes](./classes.drawio.png) + +## 整体流程 +0. 在`~/rpmbuild/SOURCES`文件夹下创建源代码文件夹,并在该文件夹下调用`apt source` +1. `DscParser`解析dsc文件获取主软件包的元数据信息 +2. `ControlParser`解析control文件获取子包的元数据信息 +3. `LicenseParser`解析license文件获取license信息 +4. 从debian/patches文件夹中获取补丁文件并复制到源代码文件夹中 +5. 调用`get_files`脚本获取安装文件列表,并完成build +6. 将元数据信息转化为spec中对应的内容 +7. 生成spec文件 +8. 将待安装文件目录复制到BUILDROOT中(即`%install`) +9. 将Source和Patch文件复制到SOURCES中 + +## 测试结果 +在Ubuntu 20.04环境下,对`apache2`、`nginx`、`redis`、`ffmpeg`、`tmux`、`nghttp2`、`zip`、`dh-python`进行了测试,均能够成功构建出rpm包及rpm源码包。 + +## 部分待处理细节 +1. **License的提取:** 目前忽略了子软件包可能存在的不同License,并且对于特殊的License文件格式未处理,默认为MIT,如zip +2. **BuildArch:** 忽略了`BuildArch`的内容,因为使用`noarch`在建包时可能会出现有架构要求的二进制文件而出错的情况,如nghttp2 + +## 迁移到openEuler需要解决的问题 +1. **源码包**:源码包对应文件可以在Source池中查询,由于该文件较大,可以考虑事先存储在数据库中 +2. **依赖**:需要填写依赖相关字段 \ No newline at end of file diff --git a/tools/deb2rpm/doc/examples.md b/tools/deb2rpm/doc/examples.md new file mode 100644 index 0000000..e7998bf --- /dev/null +++ b/tools/deb2rpm/doc/examples.md @@ -0,0 +1,137 @@ +# Examples of deb2rpm + +此处以`zip`和`screen`两个包为例,展示`deb2rpm`的使用方法。 + +## zip +### `python deb2rpm.py -s zip` 查看依赖 +``` +搜索到以下版本的zip: +1: main 3.0-12build2 +请选择所需要的版本,输入对应的标号(1~1): +``` +只有一个版本的`zip`,输入`1`继续。 + +``` +#################### zip构建依赖 ########################### +Build-Depends: + [RPM] bzip2-devel +####################################################################### +#################### zip运行依赖 ########################### +Depends: + [RPM] bzip2 + [RPM] glibc >= 2.34 +Recommends: + [DEB] unzip +####################################################################### + +``` +上述输出提供了`zip`源码包的构建依赖,以及`zip`二进制包的运行依赖和推荐依赖。其中`[RPM]`表示可直接安装或引入的依赖,`[DEB]`表示需要额外安装的依赖。 + +### `python deb2rpm.py -b zip`安装`zip`包构建依赖 +类似于`apt-get build-dep`安装所有构建依赖,执行 +``` +sudo dnf install --nogpgcheck bzip2-devel +``` +加上`--nogpgcheck`是因为有一些依赖并不在`openEuler`的官方源中,而是通过`oepkgs`的源提供的。 + +### `python deb2rpm.py zip`构建`zip`包 +构建前程序会自动检查构建依赖是否已全部安装,如果没有安装会提示安装,如果已安装则会自动下载`zip`的源码包并进行构建,默认执行source_and_binary构建,构建成功后会在`~/rpmbuild`目录下按照RPM包的目录结构生成`zip`的RPM包。 + +## screen +### `python deb2rpm.py -s screen` 查看依赖 +``` +搜索到以下版本的screen: +1: main 4.9.0-1 +请选择所需要的版本,输入对应的标号(1~1): +1 +#################### screen构建依赖 ############################ +Build-Depends: + [RPM] dpkg-devel >= 1.16.1 + [RPM] ncurses-devel + [RPM] pam-devel + [RPM] libutempter-devel + [RPM] texinfo +####################################################################### +#################### screen运行依赖 ########################### +Depends: + [RPM] glibc >= 2.34 + [DEB] libcrypt1 >= 4.1.0 + [RPM] pam >= 0.99.7.1 + [RPM] ncurses-libs >= 6 + [RPM] libutempter >= 1.1.5 +Suggests: + [DEB] byobu + [DEB] ncurses-term +#################### screen-udeb运行依赖 ########################### +####################################################################### +``` + +`screen`的依赖更为复杂,且具有一定的代表性。除了`zip`中的三类依赖外,`screen`需要额外安装的依赖还有`libcrypt1`,因此需要额外编译`libcrypt1`的源码包`libxcrypt`来安装`libcrypt1`。 + +``` +python deb2rpm.py screen -d ~/screen_dep.json +``` +```json +{ + "Name": "screen", + "Build-Depends": [ + "dpkg-devel >= 1.16.1", + "ncurses-devel", + "pam-devel", + "libutempter-devel", + "texinfo" + ], + "Packages": { + "screen": { + "Depends": [ + "glibc >= 2.34", + "libcrypt1 >= 4.1.0", + "pam >= 0.99.7.1", + "ncurses-libs >= 6", + "libutempter >= 1.1.5" + ], + "Suggests": [ + "byobu", + "ncurses-term" + ] + }, + "screen-udeb": {} + } +} +``` + +如果需要保存`screen`的全量依赖信息(即所有递归依赖),可以指定`-a`或`--all-dep-json`参数,构建完成后对应json依赖文件会保存在指定目录下,例如: +``` +python deb2rpm.py screen -a ~/screen_all_dep.json +``` +```json +{ + "screen": { + "Depth": 0, + "Build-Depends": [ + "debhelper-compat = 13", + "dpkg-dev >= 1.16.1", + "libncurses-dev", + "libpam0g-dev", + "libutempter-dev", + "texinfo" + ], + "Pre-Depends": [], + "Depends": [ + "libc6 >= 2.34", + "libcrypt1 >= 4.1.0", + "libpam0g >= 0.99.7.1", + "libtinfo6 >= 6", + "libutempter0 >= 1.1.5" + ] + }, + "libcrypt1": { + "Depth": 1, + "Build-Depends": [], + "Pre-Depends": [], + "Depends": [ + "libc6 >= 2.25" + ] + } +} +``` diff --git a/tools/deb2rpm/docs/assets/servers.png b/tools/deb2rpm/docs/assets/servers.png new file mode 100644 index 0000000000000000000000000000000000000000..b211cea3bebb154e9452dccf06dc0a6fcac87fd2 GIT binary patch literal 33634 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 literal 0 HcmV?d00001 diff --git a/tools/deb2rpm/docs/design_docs/deb2rpm-design.md b/tools/deb2rpm/docs/design_docs/deb2rpm-design.md new file mode 100644 index 0000000..d9dc65a --- /dev/null +++ b/tools/deb2rpm/docs/design_docs/deb2rpm-design.md @@ -0,0 +1,79 @@ +[TOC] + +## 1. 需求描述 + +1. 迁移需求: 将 Ubuntu(18.04)的 deb 软件包,通过工具自动化转化为在openEuler 20.03-LTS-SP3 上可运行的 rpm 软件包 +2. 兼容性需求: 转化出的 rpm 软件包,要求通过构建测试、兼容性测试 +3. 升级需求: 支持自动化迭代提升构建成功率 + +### 1.1 依赖组件 + +| 组件 | 组件描述 | 可获得性 | +| --------- | ------ | --------- | +| python3 | python3 及以上版本 | 可使用 `dnf/yum` 进行安装 | +| python3-concurrent-log-handler | logging日志辅助模块,用于日志转储 | 可使用 `dnf/yum` 进行安装 | +| dpkg | 系统上处理 deb 软件包的基础指令 | 可使用 `dnf/yum` 进行安装 | +| dpkg-devel | 提供一些开发工具,包括 dpkg-source | 可使用 `dnf/yum` 进行安装 | +| debhelper | 提供一些构建 deb 包的工具 | 配置 oepkgs 镜像源,可使用 `dnf/yum` 进行安装 | +| ruby | ruby 2.7 及以上版本 | 可使用 `dnf/yum` 进行安装 | +| ruby-devel | 提供一些 ruby 开发工具 | 可使用 `dnf/yum` 进行安装 | +| rubygems | ruby 的包管理器,功能上类似于 apt-get、yum 等 | 可使用 `dnf/yum` 进行安装 | +| fpm | linux 下的一款开源打包工具,可实现 deb 包转化成 rpm 包 | 可使用 `gem` 进行安装 | +| rpmrebuild | 用于构建 rpm 包的工具 | 可使用 `dnf/yum` 进行安装 | + +### 1.3 License + +Mulan V2 + +## 2. 设计概述 + +### 2.1 整体方案分析 + +![](../assets/servers.png) + +### 2.2 设计原则 + +- 数据与代码分离:将需求动态改变的数据提取到配置文件,而非用硬编码写死在代码里 +- 接口与实现分离:外部依赖模块接口而不是依赖模块实现 +- 模块划分:模块之间互相独立,执行互相不影响 + +## 3. 需求分析 + +### 3.1 分析思路 + +- 特性分析 + 用户拿到deb2rpm工具,有不同的功能诉求,不同的功能诉求确定了deb2rpm实现逻辑上的差异: + 1. 构建二进制rpm包:输入是一个deb包,想基于deb2rpm-server的OS环境构建出一个rpm包,同时提供 “忽略安装依赖” 的构建参数,以保证构建生成的rpm,在安装时不会由于缺少安装依赖无法安装 + 2. 构建二进制rpm包及src.rpm包:输入是一个deb包,想基于deb2rpm-server的OS环境,通过工具转换生成spec文件,从而构建生成rpm包以及src.rpm包,转换生成的spec文件,通过包名映射数据表,修改软件包的构建及安装依赖包名,以提高构建成功率 + 3. deb包转化依赖扫描:输入是一个deb包,基于deb2rpm-client以及deb2rpm-server环境进行构建、安装依赖扫描,识别依赖是否存在缺失 + 4. 构建出的rpm包镜像源发布:将构建好的rpm存放在固定目录,并构建自己的镜像源,用于后续依赖问题解决 + +- 用例视图 + +![](../assets/user-case.png) + + +#### 3.1.1 deb2rpm-client + +- 模块介绍 + 获取deb包的源码,通过处理源码包中的控制文件,解析deb软件包的构建依赖,基于包名映射数据表解决构建依赖。通过debhelper工具,完成软件包编译 + +#### 3.1.2 deb2rpm-server + + +### 3.2 框架设计 +``` +. +├── deb2rpm +├── scripts 工具脚本 +├── server 服务端 +├── config 配置文件 +│ ├── version.config deb2rpm 版本信息配置文件, 用于后续版本演进 +│ ├── source.list deb 软件包镜像源获取地址 +│ ├── deb2rpm.repo rpm 软件包镜像源获取地址 +│ └── debian2openeuler.json 包名映射配置文件 +├── templates 提供配置文件,被测试框架 lkp-tests 所集成 +└── tests 测试用例 +``` + + diff --git a/tools/deb2rpm/initialize.sh b/tools/deb2rpm/initialize.sh new file mode 100755 index 0000000..9297f88 --- /dev/null +++ b/tools/deb2rpm/initialize.sh @@ -0,0 +1,69 @@ +#!/bin/bash +deb2rpm_directory="$(dirname "$0")" +home_dir="$HOME" + +version="" +url="" + +while getopts ":v:u:" opt; do + case $opt in + v) + version="$OPTARG" + ;; + u) + url="$OPTARG" + ;; + \?) + echo "Illegal param: -$OPTARG" >&2 + exit 1 + ;; + esac +done + +shift $((OPTIND-1)) + +if [ -n "$version" ]; then + echo "Specify version: $version" +else + version="jammy" + echo "Default version: $version" +fi + +if [ -n "$url" ]; then + echo "Specify url: $url" +else + url="https://repo.huaweicloud.com/ubuntu" + echo "Default url: $url" +fi + +echo "Ensure the project is under your home directory." +mv -n "$deb2rpm_directory" "$home_dir" + +echo "Install the required dependency for deb2rpm" +echo "Installing python and networkx" +sudo dnf install python3 python3-pip +pip install networkx + +echo "Installing rpm-build" +sudo dnf install rpm-build + +echo "Add repos of oepkgs to enable deb" +sudo dnf config-manager --add-repo https://repo.oepkgs.net/openeuler/rpm/openEuler-22.03-LTS/extras/aarch64/ +sudo dnf config-manager --add-repo https://repo.oepkgs.net/openeuler/rpm/openEuler-22.03-LTS/compatible/aur/x86_64/ +sudo dnf config-manager --add-repo https://repo.oepkgs.net/openeuler/rpm/openEuler-22.03-LTS/compatible/f33/x86_64/ +dnf clean all && dnf makecache +sudo dnf install --nogpgcheck debhelper dpkg dpkg-devel + +echo "Get repo files of $url" +python3 ~/deb2rpm/scripts/get_repos.py "$version" "$url" + +echo "Initialize the database" +if [ ! -f "$home_dir/deb2rpm/database/deb2rpm.db" ]; then + touch ~/deb2rpm/database/deb2rpm.db + echo "Create a database: ~/deb2rpm/database/deb2rpm.db" +else + echo "Database exists" +fi + +python3 ~/deb2rpm/scripts/init_db.py deb2rpm_test +echo "Finishing initializing!" diff --git a/tools/deb2rpm/scripts/deb_parser.py b/tools/deb2rpm/scripts/deb_parser.py new file mode 100644 index 0000000..c19fea3 --- /dev/null +++ b/tools/deb2rpm/scripts/deb_parser.py @@ -0,0 +1,166 @@ +import os +import re +import glob +import shutil +import logging + +from typing import Union, List + +logger = logging.getLogger(__name__) +dsc_keywords = ['Source', 'Version', 'Homepage'] + +def get_line_info(line: str) -> List: + """ + 从line中提取信息 + @param line 一行,格式为key: value + @return 返回[key, value] + """ + pattern = r'^(.*?):\s*(.*)' + match = re.search(pattern, line) + if match: + return match.group(1).strip(), match.group(2).strip() + return "", "" + +class DscParser: + def __init__(self, path: str): + """ + 初始化DscParser,获取元数据和Source信息 + @param path .dsc文件路径 + """ + self.meta_data = {} + + get_file = False + with open(path, 'r') as file: + for line in file: + key, value = get_line_info(line) + if key == 'Checksums-Sha256': + get_file = False + break + if line.strip() == "": + get_file = False + elif key in dsc_keywords: + self.meta_data[key] = value + elif key == 'Checksums-Sha1': + get_file = True + self.meta_data['Files'] = [] + elif get_file: + parts = line.strip().split() + self.meta_data['Files'].append(parts[-1]) + +class ControlParser: + def __init__(self, path: str): + """ + 初始化ControlParser,获取子包信息 + @param path control文件路径 + """ + self.package_data = {} + + with open(path, 'r') as file: + package_name = "" + description = False + + for line in file: + key, value = get_line_info(line) + if key == "Package": + description = False + package_name = value + self.package_data[package_name] = {} + elif key == "Architecture": + if value == 'any' or value == 'all': + self.package_data[package_name].update({key: "noarch"}) + else: + self.package_data[package_name].update({key: value}) + elif key == "Description": + self.package_data[package_name].update({"Summary": value}) + description = True + self.package_data[package_name]["description"] = "" + elif description: + self.package_data[package_name]["description"] += line + elif key != '' and value != '': + logger.warning(f'Ignore {key}: {value}') + +class LicenseParser: + def __init__(self, path: str): + """ + 初始化LicenseParser,获取License名(默认MIT) + @param path copyright文件路径 + """ + self.license = "" + + if os.path.exists(path): + capture = False + with open(path, 'r') as file: + for line in file: + if line.strip() == "Files: *": + capture = True + continue + if capture: + match = re.search(r'License: (.*)', line) + if match: + self.license = match.group(1).strip() + break + + if not capture: + self.license = 'MIT' + else: + self.license = 'MIT' + + +class DebParser: + def __init__(self, path: str, build_path: str): + """ + 初始化DebParser + @param path 源码包目录(即包含dsc的目录) + @param build_path build目录 + """ + self.meta_data = {} + self.control_data = {} + + dsc_files = self.get_certain_files(path, ".dsc", False) + + assert len(dsc_files) == 1, 'dsc错误!' + self.dsc_name = dsc_files[0] + dsc = DscParser(self.dsc_name) + + control = ControlParser(os.path.join(build_path, "debian/control")) + lic = LicenseParser(os.path.join(build_path, "debian/copyright")) + + # 获取patch并复制到源文件夹中 + patch_path = os.path.join(build_path, "debian/patches") + self.patches = [] + if os.path.exists(patch_path): + with open(os.path.join(patch_path, "series"), "r") as patches_file: + for line in patches_file: + if line.strip() == "": + break + if line[0] == "#": + continue + self.patches.append(line.strip()) + for patch in self.get_certain_files(patch_path, ".patch", True): + shutil.copy2(patch, path) + for diff in self.get_certain_files(patch_path, ".diff", True): + shutil.copy2(diff, path) + + self.meta_data.update(dsc.meta_data) + self.meta_data['Files'].append(os.path.basename(self.dsc_name)) + self.control_data = control.package_data + self.meta_data['copyright'] = lic.license + + def get_certain_files(self, dir: str, pattern: str, get_sub: bool) -> List: + all_files = os.listdir(dir) + files = [os.path.join(dir, file) for file in all_files if file.endswith(pattern)] + # print(files) + if get_sub: + sub_dirs = [os.path.join(dir, sub_dir) for sub_dir in all_files if os.path.isdir(os.path.join(dir, sub_dir))] + for sub in sub_dirs: + files += self.get_certain_files(sub, pattern, True) + return files + +if __name__ == "__main__": + parser = DebParser('/home/young/rpmbuild/SOURCES/nghttp2/', '/home/young/rpmbuild/BUILD/nghttp2-1.43.0/') + print(parser.meta_data) + # print(parser.control_data) + for package_name, package_info_dir in parser.control_data.items(): + print(package_name) + for key, value in package_info_dir.items(): + print(f'{key}: {value}') diff --git a/tools/deb2rpm/scripts/get_dependency.py b/tools/deb2rpm/scripts/get_dependency.py new file mode 100644 index 0000000..67471d5 --- /dev/null +++ b/tools/deb2rpm/scripts/get_dependency.py @@ -0,0 +1,139 @@ +import re +import os +import sys +import json + +from scripts.init_db import InitDB + +home = os.getenv("HOME") +mapping_path = f"{home}/deb2rpm/config/deb2rpm_mapping.json" + +package_info_keys = ["Package", "Provides", "PreDepends", "Depends", "Recommends", "Suggests", "Conflicts", "Obsoletes", "Source"] + +class DepParser: + def __init__(self, all_dep: str, map=False): + """ + 初始化依赖提取器 + @param all_dep 数据库中提取出的原始字符串 + """ + self.deps_and_version = [] + self.map = map + with open(mapping_path, 'r') as file: + self.mapping = json.load(file) + if all_dep != '': + for dep in all_dep.strip().split(", "): + self.process_each(dep) + + def process_each(self, deps: str) -> None: + """ + 依次处理每一段字符串 + @param deps 单个的依赖 + """ + # | 分隔的不同依赖选用第一个 + parts = deps.split('|') + dep = parts[0] + + # 忽略[]中的架构信息和<>中的其它信息 + dep_ignore1 = re.sub(r'\[[^\]]*\]', '', dep).strip() + dep_ignore0 = re.sub(r'<[^>]*>', '', dep_ignore1).strip() + + # 提取依赖名及版本信息 + dep_ignore = dep_ignore0.replace('(', '').replace(')', '') + dep_parts = dep_ignore.split() + + # 排除类似python3:any中的架构信息干扰 + names = dep_parts[0].split(":") + pkg_name = self.map_name(names[0]) + if pkg_name == "debhelper": + return + if len(dep_parts) == 3: + version = self.get_version(dep_parts[2]) + if dep_parts[1] == "<<": + self.deps_and_version.append(f"{pkg_name} < {version}") + elif dep_parts[1] == ">>": + self.deps_and_version.append(f"{pkg_name} > {version}") + else: + self.deps_and_version.append(f"{pkg_name} {dep_parts[1]} {version}") + else: + self.deps_and_version.append(pkg_name) + + def map_name(self, name: str) -> str: + """ + 包名映射 + @param name deb包名 + @return 对应rpm包名 + """ + if self.map and name in self.mapping.keys(): + return self.mapping[name] + # if name.endswith("-dev"): + # return f"{name}el" + return name + + def get_version(self, deb_version: str) -> str: + """ + 从deb版本号中提取version信息 + @param deb_version deb版本字符串 + """ + version_string = deb_version + epoch_pattern = r'^(\d+):' + release_pattern = r'(.*?)[-+~](.*)' + + match_epoch = re.match(epoch_pattern, deb_version) + if match_epoch: + epoch = match_epoch.group(1) + version_string = version_string[len(epoch)+1:] + match_release = re.match(release_pattern, version_string) + if match_release: + release = match_release.group(2).strip() + version_string = version_string[:-len(release)-1] + + return version_string + +class DepJson: + def __init__(self, repo_name: str, source_name: str, json_path=""): + """ + 初始化DepJson,获取所有信息 + @param repo_name 源名 + @param source_name 源码包名 + @param json_path 生成json文件所在文件夹路径(默认为当前路径) + """ + self.repo_name = repo_name + self.source_name = source_name + self.json_path = json_path + self.dic = {} + db = InitDB() + _, _, build_requires, _, packages, _ = db.get_source_info(self.repo_name, source_name) + + src = DepParser(build_requires, map=True) + build_requires_list = src.deps_and_version + self.dic["Name"] = source_name + self.dic["Build-Depends"] = build_requires_list + + self.dic["Packages"] = {} + for package in packages.strip().split(): + self.dic["Packages"][package]= {} + pac_info = db.get_package_info(self.repo_name, package) + if pac_info != None: + for i, value in enumerate(pac_info): + if i != 0 and i != 8 and i != 9 and value != "": + dep = DepParser(value, map=True) + self.dic["Packages"][package][package_info_keys[i]] = dep.deps_and_version + + # print(self.dic) + db.close_db() + + def write_into_json(self) -> None: + if self.json_path == "": + self.json_path = os.path.join(os.getcwd(), f"{self.source_name}.json") + + with open(self.json_path, 'w') as file: + json.dump(self.dic, file, indent=4, ensure_ascii=False) + + + +if __name__ == "__main__": + assert len(sys.argv) == 2 + target = sys.argv[1] + + dj = DepJson("main", target) + dj.write_into_json() diff --git a/tools/deb2rpm/scripts/get_files.py b/tools/deb2rpm/scripts/get_files.py new file mode 100644 index 0000000..eaeb1ee --- /dev/null +++ b/tools/deb2rpm/scripts/get_files.py @@ -0,0 +1,453 @@ +import os +import sys +import re +import json +import logging +import shutil +<<<<<<< HEAD +======= +# import subprocess +>>>>>>> 6e53b74 (Change a method to extract files from deb) + +from scripts.init_db import InitDB + +from typing import List + +logger = logging.getLogger(__name__) + +home = os.getenv('HOME') + +json_path = f"{home}/deb2rpm/config/resources.json" +with open(json_path, 'r') as file: + json_data = json.load(file) +web_repo = json_data["repo_url"] + +def get_line_info(line: str) -> List: + """ + 从line中提取信息 + @param line 一行,格式为key: value + @return 返回[key, value] + """ + pattern = r'^(.*?):\s*(.*)' + match = re.search(pattern, line) + if match: + return match.group(1).strip(), match.group(2).strip() + return "", "" + +all_commands = [ + "debian/rules build", + "dh_testroot", + "dh_prep", + "dh_installdirs", + "dh_auto_install", + "dh_install", + "dh_installdocs", + "dh_installchangelogs", + "dh_installexamples", + "dh_installman", + "dh_installcatalogs", + "dh_installcron", + "dh_installdebconf", + "dh_installemacsen", + "dh_installifupdown", + "dh_installinfo", + "dh_installinit", + "dh_installsystemd", + "dh_installmenu", + "dh_installmime", + "dh_installmodules", + "dh_installlogcheck", + "dh_installlogrotate", + "dh_installpam", + "dh_installppp", + "dh_installudev", + "dh_installwm", + "dh_installxfonts", + "dh_bugfiles", + "dh_lintian", + # "dh_gconf", + "dh_icons", + "dh_installsystemduser", + "dh_perl", + "dh_usrlocal", + "dh_link", + "dh_compress", + "dh_fixperms", + "dh_missing", + "dh_dwz", + "dh_strip", +# "dh_makeshlibs", +# "dh_shlibdeps", +# "dh_installdeb", +# "dh_gencontrol", +# "dh_md5sums", +# "dh_builddeb", +] + +class DebFiles: + + def __init__(self, path: str, repo_name: str, source_name: str, download_debs: bool) -> None: + """ + @param path: deb源码包根目录 + 在path目录下执行操作获取信息 + """ + self.root_path = path + self.repo = repo_name + self.source = source_name + self.rules_targets = [] + self.files = {} + self.subdirs = {} + self.arch = {} + self.commands = all_commands # self.all_params = "" + os.chdir(path) + self._get_rules_targets() + # print(self.rules_targets) + self._refresh_commands() + self._get_packages() + if not download_debs: + self._exe_commands() + self._get_all_files() + else: + self._get_files_by_debs() + + # print(self.files) + # print(self.subdirs) + # print(self.arch) +<<<<<<< HEAD +======= + +>>>>>>> ba33252 (Hide part of output in get_files) + + def _get_rules_targets(self) -> None: + """ + 解析debian/rules文件,将所有target存入self.rules_targets + 并获取dh $@后的通用参数 + """ + pattern = r'(.*?):[^=](.*)' + pattern2 = r'dh\s+\$@\s+(.*)' + with open("debian/rules", 'r') as file: + for line in file: + if line.startswith('#'): + continue + match = re.search(pattern, line) + if match: + self.rules_targets.append(match.group(1).strip()) + + match2 = re.search(pattern2, line) + if match2 and match2.group(1).strip() != "": + logger.info(f"dh binary阶段忽略参数{match2.group(1).strip()}") + # self.all_params = match2.group(1).strip() + + def _refresh_commands(self) -> None: + """ + 根据rules文件中的target,修改commands的内容 + """ + to_be_changed = {} + for c in all_commands: + to_be_changed[c] = [] + + # 获取override字段 + pattern1 = r'override_(.*)' + pattern2 = r'(.*?)(?:-indep|-arch)' + for target in self.rules_targets: + match = re.search(pattern1, target) + if match: + match2 = re.search(pattern2, match.group(1).strip()) + if match.group(1).strip() in all_commands: + to_be_changed[match.group(1).strip()].append(f"debian/rules {target}") + elif match2 and match2.group(1).strip() in all_commands: + to_be_changed[match2.group(1).strip()].append(f"debian/rules {target}") + + # 更新命令 + for key, value_list in to_be_changed.items(): + if len(value_list) != 0: + i = self.commands.index(key) + self.commands = self.commands[:i] + value_list + self.commands[i+1:] + # print(self.commands) + # 有关清理任务 + if "override_dh_auto_clean" in self.rules_targets: + self.commands.insert(0, "debian/rules override_dh_auto_clean") + else: + self.commands.insert(0, "debian/rules clean") + + def _exe_commands(self) -> None: + for command in self.commands: + logger.info(f"执行命令 fakeroot {command}") + res = os.system("fakeroot " + command) + if res != 0: + logger.error(f"执行{command}失败!") + sys.exit() + + def _get_packages(self) -> None: + """ + 从debian/control文件中获取所有子包名 + """ + package = "" + with open("debian/control", 'r') as file: + for line in file: + if line.startswith('#'): + continue + key, value = get_line_info(line) + if key == "Package": + package = value + self.files[package] = [] + self.subdirs[package] = [] + self.arch[package] = "" + elif key == "Architecture": + if value == "any": + self.arch[package] = "amd64" + else: + self.arch[package] = value + + def _get_all_files(self) -> None: + """ + 将子包所有的文件添加到files字典中 + """ + for p in self.files.keys(): + path = os.path.join(self.root_path, "debian", p) + self.files[p], self.subdirs[p] = self._get_all_files_under_dir(path) + + def _get_all_files_under_dir(self, path: str) -> List: + """ + @param path 目标文件夹 + @return 目标文件夹下所有文件的列表 + """ + all_files = [] + empty_dir = [] + for root, subdirs, files in os.walk(path): + if 'DEBIAN' in subdirs: + subdirs.remove("DEBIAN") + if len(files) == 0 and len(subdirs) == 0: + empty_dir.append(os.path.relpath(root, path)) + for file in files: + abs_path = os.path.join(root, file) + all_files.append(os.path.relpath(abs_path, path)) + + return all_files, empty_dir + + def _get_files_by_debs(self): + db = InitDB() +<<<<<<< HEAD +<<<<<<< HEAD + _, _, _, _, packages, _ = db.get_source_info(self.repo, self.source) + cur_dir = os.getcwd() + os.makedirs("debs", exist_ok=True) + os.chdir("debs") +======= + _, version, _, dir, packages, _ = db.get_source_info(self.repo, self.source) + version = version[version.find(':')+1:] +<<<<<<< HEAD + os.system("mkdir debs && cd debs") +>>>>>>> ba33252 (Hide part of output in get_files) +======= + cur_dir = os.getcwd() + os.makedirs("debs", exist_ok=True) + os.chdir("debs") + # os.system("mkdir debs && cd debs") +>>>>>>> 5d59dad (Fix bugs in getting files) + for package in packages.strip().split(): + pac_info = db.get_package_info(self.repo, package) + if not pac_info: + logger.error(f"Didn't find deb of {package}") + sys.exit() +======= + _, _, _, _, packages, _ = db.get_source_info(self.repo, self.source) + cur_dir = os.getcwd() + os.makedirs("debs", exist_ok=True) + os.chdir("debs") + for package in packages.strip().split(): + pac_info = db.get_package_info(self.repo, package) +<<<<<<< HEAD +>>>>>>> 8dba3e5 (Add path of deb to database) +======= + if not pac_info: + logger.error(f"Didn't find deb of {package}") + sys.exit() +>>>>>>> 4c88c73 (Exit when hitting a Not Found of deb) + deb_dir_and_name = pac_info[-1] + deb_name = list(deb_dir_and_name.split('/'))[-1] + web_path = f"{web_repo}/{deb_dir_and_name}" + # os.system("pwd") + res = os.system(f"wget {web_path}") + if res != 0: + logger.error(f"未找到{web_path}!") +<<<<<<< HEAD +<<<<<<< HEAD + sys.exit() + else: + self.files[package], self.subdirs[package] = self._files_in_deb(deb_name) + # os.remove(f"{deb_name}") + + os.chdir(cur_dir) +======= + if self.arch[package] == "all": + alter_deb_name = f"{package}_{version}_amd64.deb" + res2 = os.system(f"wget {web_repo}/{dir}/{alter_deb_name}") + if res2 != 0: + logger.error(f"未找到 {web_repo}/{dir}/{alter_deb_name}") + sys.exit() + else: + self.files[package], self.subdirs[package] = self._files_in_deb(alter_deb_name) + # os.remove(f"{alter_deb_name}") + else: + sys.exit() +======= + sys.exit() +>>>>>>> 8dba3e5 (Add path of deb to database) + else: + self.files[package], self.subdirs[package] = self._files_in_deb(deb_name) + # os.remove(f"{deb_name}") +>>>>>>> 5d59dad (Fix bugs in getting files) + + os.chdir(cur_dir) + + def _files_in_deb(self, path) -> List: + os.makedirs(f'/tmp/{self.source}', exist_ok=True) + name = os.path.basename(path) + if not os.path.exists(f'/tmp/{self.source}/{name}'): + shutil.move(path, f'/tmp/{self.source}') + cur_dir = os.getcwd() + os.chdir(f'/tmp/{self.source}') + ret = os.popen(f'ar x {name}') + ret.read() + ret = os.popen(f'zstd -d -c data.tar.zst | tar -vxf - ') +<<<<<<< HEAD +<<<<<<< HEAD +<<<<<<< HEAD + fileTree = FileNode("") +<<<<<<< HEAD + + for line in ret.read().split('\n'): + parts = line.split('/') + path_parts = [part for part in parts if part and part != '.'] +======= + # print(output) + for line in output.strip().split('\n'): + path = list(line.split())[5][1:] + # print(path) + parts = path.split('/') + path_parts = [part for part in parts if part] + # print(path_parts) +>>>>>>> ba33252 (Hide part of output in get_files) + current_node = fileTree + for i, path_part in enumerate(path_parts): + if i != len(path_parts) - 1: + if path_part not in current_node.children.keys(): + current_node.children[path_part] = FileNode(path_part) + current_node = current_node.children[path_part] + else: + dir = path.endswith('/') + if path_part not in current_node.children.keys(): + current_node.children[path_part] = FileNode(path_part, dir) + os.chdir(cur_dir) + return fileTree.get_files_and_dirs() + + +class FileNode: + def __init__(self, name: str, dir=True): + self.name = name + self.children = {} + self.dir = dir + + def get_files_and_dirs(self) -> List: + if len(self.children) == 0: + if self.dir: + return [], [f"{self.name}"] + else: + return [f"{self.name}"], [] + +======= +>>>>>>> 6e53b74 (Change a method to extract files from deb) + files = [] + dirs = [] + for line in ret.read().split('\n'): + parts = line.split('/') + path_parts = [part for part in parts if part and part != '.'] + if not path_parts: + continue + abs_path = f'/tmp/{self.source}' + if os.path.isdir(abs_path): + dirs.append(abs_path) + else: + files.append(abs_path) + os.chdir(cur_dir) + return files, dirs +======= + # files = [] + # dirs = [] + # for line in ret.read().split('\n'): + # parts = line.split('/') + # path_parts = [part for part in parts if part and part != '.'] + # if not path_parts: + # continue + # abs_path = f'/tmp/{self.source}' + # if os.path.isdir(abs_path): + # dirs.append(abs_path) + # else: + # files.append(abs_path) + # os.chdir(cur_dir) + # return files, dirs +>>>>>>> 5d59dad (Fix bugs in getting files) + + # try: + # output = subprocess.check_output(['dpkg', '-c', path], universal_newlines=True) + # except Exception as e: + # print(f"dpkg -c Error: {e}") + # +======= +>>>>>>> 4c88c73 (Exit when hitting a Not Found of deb) + fileTree = FileNode("") + + for line in ret.read().split('\n'): + parts = line.split('/') + path_parts = [part for part in parts if part and part != '.'] + current_node = fileTree + for i, path_part in enumerate(path_parts): + if i != len(path_parts) - 1: + if path_part not in current_node.children.keys(): + current_node.children[path_part] = FileNode(path_part) + current_node = current_node.children[path_part] + else: + dir = path.endswith('/') + if path_part not in current_node.children.keys(): + current_node.children[path_part] = FileNode(path_part, dir) + os.chdir(cur_dir) + return fileTree.get_files_and_dirs() + + +class FileNode: + def __init__(self, name: str, dir=True): + self.name = name + self.children = {} + self.dir = dir + + def get_files_and_dirs(self) -> List: + if len(self.children) == 0: + if self.dir: + return [], [f"{self.name}"] + else: + return [f"{self.name}"], [] + + files = [] + dirs = [] + for child in self.children.values(): + files += [f"{self.name}/{file}" for file in list(child.get_files_and_dirs())[0]] + dirs += [f"{self.name}/{dir}" for dir in list(child.get_files_and_dirs())[1]] + + return files, dirs + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Provide one directory!") + sys.exit() + + root_dir = os.getcwd() + package_dir = os.path.join(root_dir, sys.argv[1]) + all_subdir = [os.path.join(package_dir, x) for x in os.listdir(package_dir) + if os.path.isdir(os.path.join(package_dir, x))] + + if len(all_subdir) == 1: + DebFiles(all_subdir[0], "main", sys.argv[1], True) + # os.chdir("dh-python/dh-python-5.20220403") + # os.system("fakeroot debian/rules clean") + # os.system("fakeroot debian/rules build") diff --git a/tools/deb2rpm/scripts/get_repos.py b/tools/deb2rpm/scripts/get_repos.py new file mode 100644 index 0000000..d6b1105 --- /dev/null +++ b/tools/deb2rpm/scripts/get_repos.py @@ -0,0 +1,57 @@ +import os +import sys +import json + +home = os.getenv("HOME") + +cates = ['', '-updates', '-backports', '-security', '-proposed'] +repos = ['main', 'multiverse', 'restricted', 'universe'] + +def create_dic(name: str): + os.system(f"rm -rf {home}/Package20 && mkdir {home}/Package20") + os.system(f"cd {home}/Package20 && mkdir " + ' '.join([f"{name}"+i for i in cates])) + os.system(f"rm -rf {home}/Source20 && mkdir {home}/Source20") + os.system(f"cd {home}/Source20 && mkdir " + ' '.join([f"{name}"+i for i in cates])) + +def get_source_package(name: str, url: str, cate: str, repo: str): + source_url = url + '/dists/' + f'{name+cate}/{repo}/source/Sources.gz' + package_url = url + '/dists/' + f'{name+cate}/{repo}/binary-amd64/Packages.gz' + + os.system(f"cd {home}/Source20/{name+cate} && wget {source_url}") + os.system(f"gzip -d {home}/Source20/{name+cate}/Sources.gz") + os.system(f"mv {home}/Source20/{name+cate}/Sources {home}/Source20/{name+cate}/{'Sources-'+repo}") + + os.system(f"cd {home}/Package20/{name+cate} && wget {package_url}") + os.system(f"gzip -d {home}/Package20/{name+cate}/Packages.gz") + os.system(f"mv {home}/Package20/{name+cate}/Packages {home}/Package20/{name+cate}/{'Packages-'+repo}") + +if __name__ == "__main__": + assert len(sys.argv) == 3 + version_name = sys.argv[1] + url = sys.argv[2] + create_dic(version_name) + for cate in cates: + for repo in repos: + get_source_package(version_name, url, cate, repo) + + json_data = {} + json_data["refresh"] = False + json_data["repo_url"] = url + json_data["main"] = {} + for cate in cates: + vn = version_name + cate + if cate == '': + cate = '-main' + + cate_name = cate[1:] + json_data[cate_name] = {} + json_data[cate_name]["enable"] = True + json_data[cate_name]["Sources"] = {} + for repo in repos: + json_data[cate_name]["Sources"][repo] = f"{home}/Source20/{vn}/Sources-{repo}" + json_data[cate_name]["Packages"] = {} + for repo in repos: + json_data[cate_name]["Packages"][repo] = f"{home}/Package20/{vn}/Packages-{repo}" + + with open(f"{home}/deb2rpm/config/resources.json", 'w') as file: + json.dump(json_data, file, indent=4, ensure_ascii=False) diff --git a/tools/deb2rpm/scripts/init_db.py b/tools/deb2rpm/scripts/init_db.py new file mode 100644 index 0000000..72fe665 --- /dev/null +++ b/tools/deb2rpm/scripts/init_db.py @@ -0,0 +1,138 @@ +import os +import sys +import json +import sqlite3 +import logging + +from typing import List + +project_path = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) +sys.path.append(project_path) + +from scripts.sqlite_database import BaseDB, SourceDB, PackageDB + +home = os.getenv("HOME") +logging.basicConfig(level=logging.INFO, + format='%(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +json_file = f"{home}/deb2rpm/config/resources.json" + +repos = ['main', 'updates', 'backports', 'security', 'proposed'] + +paths_table_name = "paths" +sources_table_names = ['SourcesMain', 'SourcesMultiverse', 'SourcesRestricted', 'SourcesUniverse'] +packages_table_names = ['PackagesMain', 'PackagesMultiverse', 'PackagesRestricted', 'PackagesUniverse'] +resources_table_names = sources_table_names + packages_table_names + +paths_table_keys = ['source_or_package', 'main', 'multiverse', 'restricted', 'universe'] +paths_table_types = ['TinyText', 'TinyText', 'TinyText', 'TinyText', 'TinyText'] + +class InitDB(BaseDB): + def __init__(self): + super().__init__() + with open(json_file, 'r') as file: + json_data = json.load(file) + refresh = json_data['refresh'] + self.enables = [json_data[repo]['enable'] for repo in repos] + self.sources_path = {} + self.packages_path = {} + + for repo in repos: + self.sources_path[repo] = json_data[repo]['Sources'] + self.packages_path[repo] = json_data[repo]['Packages'] + + for repo in repos: + if refresh or not super().table_exists(repo + paths_table_name): + for name in resources_table_names: + super().drop_table(repo + name) + self.initial_SP_tables(repo) + else: + origin_sources = super().select_data(repo + paths_table_name, 'source_or_package', 'Sources') + cnt = 1 + for name, path in zip(sources_table_names, self.sources_path[repo].values()): + if origin_sources[cnt] != path: + super().drop_table(repo + name) + SourceDB(repo + name, path) + logger.info(f"{repo}{name}表完成更新!") + cnt += 1 + + origin_packages = super().select_data(repo + paths_table_name, 'source_or_package', 'Packages') + cnt = 1 + for name, path in zip(packages_table_names, self.packages_path[repo].values()): + if origin_packages[cnt] != path: + super().drop_table(repo + name) + PackageDB(repo + name, path) + logger.info(f"{repo}{name}表完成更新!") + cnt += 1 + + self.initial_paths_table(repo) + + def initial_paths_table(self, repo_name: str): + name = repo_name + paths_table_name + super().drop_table(name) + super().create_table(name, paths_table_keys, paths_table_types) + super().insert_data(name, ['Sources'] + list(self.sources_path[repo_name].values())) + super().insert_data(name, ['Packages'] + list(self.packages_path[repo_name].values())) + + def initial_SP_tables(self, repo: str): + for name, source in zip(sources_table_names, self.sources_path[repo].values()): + SourceDB(repo + name, source) + logger.info(f"{repo}{name}表初始化完成!") + for name, package in zip(packages_table_names, self.packages_path[repo].values()): + PackageDB(repo + name, package) + logger.info(f"{repo}{name}表初始化完成!") + + def get_source_info(self, repo_name:str, source_name: str) -> List: + """ + 获取数据库中Source信息的接口 + @param repo_name 库名 + @param source_name source名 + """ + query = f''' + SELECT * FROM {repo_name + sources_table_names[0]} WHERE Package = '{source_name}' + UNION + SELECT * FROM {repo_name + sources_table_names[1]} WHERE Package = '{source_name}' + UNION + SELECT * FROM {repo_name + sources_table_names[2]} WHERE Package = '{source_name}' + UNION + SELECT * FROM {repo_name + sources_table_names[3]} WHERE Package = '{source_name}' + ''' + self.cursor.execute(query) + return self.cursor.fetchone() + + def get_package_info(self, repo_name: str, package_name: str) -> List: + """ + 获取数据库中Package信息的接口 + @param repo_name 库名 + @param package_name package名 + """ + query = f''' + SELECT * FROM {repo_name + packages_table_names[0]} WHERE Package = '{package_name}' + UNION + SELECT * FROM {repo_name + packages_table_names[1]} WHERE Package = '{package_name}' + UNION + SELECT * FROM {repo_name + packages_table_names[2]} WHERE Package = '{package_name}' + UNION + SELECT * FROM {repo_name + packages_table_names[3]} WHERE Package = '{package_name}' + ''' + self.cursor.execute(query) + return self.cursor.fetchone() + + def close_db(self): + self.conn.close() + +if __name__ == "__main__": + assert len(sys.argv) == 2 + target = sys.argv[1] + db = InitDB() + if target == "deb2rpm_test": + print("Initialize the database successfully!") + else: + source_info = db.get_source_info("main", target) + package_info = db.get_package_info("main", target) + # print(source_info[0]) + print(source_info) + # print(package_info[0]) + print(package_info) + db.close_db() diff --git a/tools/deb2rpm/scripts/json2spec.py b/tools/deb2rpm/scripts/json2spec.py new file mode 100644 index 0000000..038b46f --- /dev/null +++ b/tools/deb2rpm/scripts/json2spec.py @@ -0,0 +1,41 @@ +import os +import json + +class Json2Spec: + def __init__(self, spec_path: str, json_path: str): + """ + 初始化Json2Spec + @param spec_path spec文件路径 + @param json_path json文件路径 + """ + with open(json_path, 'r') as json_file: + self.dic = json.load(json_file) + + with open(spec_path, 'r') as file: + lines = file.readlines() + + with open(spec_path, 'w') as spec_file: + for line in lines: + if line.strip() == "# BuildRequires": + for br in self.dic["Build-Depends"]: + spec_file.write(f"BuildRequires: {br}\n") + if self.dic["Name"] in self.dic["Packages"].keys(): + self.write_package_deps(self.dic["Name"], spec_file) + elif line.startswith("# dep"): + parts = line.strip().split() + package_name = parts[-1] + self.write_package_deps(package_name, spec_file) + else: + spec_file.write(line) + + def write_package_deps(self, package_name: str, spec_file): + for key, dep_list in self.dic["Packages"][package_name].items(): + for dep in dep_list: + if key == 'PreDepends' or key == 'Depends': + spec_file.write(f"Requires: {dep}\n") + else: + spec_file.write(f"{key}: {dep}\n") + +if __name__ == "__main__": + home = os.getenv("HOME") + Json2Spec(f"{home}/rpmbuild/SPECS/dh-python-5.20220403.spec", f"{home}/rpmbuild/SPECS/dh-python.json") diff --git a/tools/deb2rpm/scripts/recursive_depends.py b/tools/deb2rpm/scripts/recursive_depends.py new file mode 100644 index 0000000..4b570ee --- /dev/null +++ b/tools/deb2rpm/scripts/recursive_depends.py @@ -0,0 +1,252 @@ +import os +import sys +import json +import logging +import networkx as nx + +from typing import List + +from scripts.init_db import InitDB +from scripts.get_dependency import DepParser + +home = os.getenv("HOME") +mapping_path = f"{home}/deb2rpm/config/deb2rpm_mapping.json" + +logger = logging.getLogger(__name__) + +class DeepDep: + def __init__(self, repo_name: str, target: str, json_path=""): + """ + 初始化DeepDep类,递归获取对应包的所有编译和运行依赖 + @param repo_name 源名 + @param target 目标包 + """ + self.target = target + self.repo_name = repo_name + self.json_path = json_path + self.data_dic = {} + self.package_order = [] + self.source_order = [] + self.source_and_package = {} + self.dep_tree = DepTree(target) + self.exists = [] + self.db = InitDB() + + with open(mapping_path, 'r') as file: + self.mapping = json.load(file) + + if self.target not in self.mapping.keys(): + self.recursive_deps(target, 0) + + self.install_order() + self.get_source_order() + + + def recursive_deps(self, package_name: str, depth: int): + """ + 递归获取依赖包的依赖 + @param package_name 包名 + @param depth 依赖深度(目标为0) + """ + # print(package_name) + if package_name in self.data_dic.keys(): + if depth > self.data_dic[package_name]["Depth"]: + self.data_dic[package_name]["Depth"] = depth + return + + # print(f"正在处理 {package_name}") + logger.info(f"正在解析 {package_name}") + self.data_dic[package_name] = {} + self.data_dic[package_name]["Depth"] = depth + self.data_dic[package_name]["Build-Depends"] = [] + self.data_dic[package_name]["Pre-Depends"] = [] + self.data_dic[package_name]["Depends"] = [] + + src = self.db.get_source_info(self.repo_name, package_name) + if (src != None): + src_build_deps = DepParser(src[2]) + if len(src_build_deps.deps_and_version) != 0: + for bd in src_build_deps.deps_and_version: + parts = bd.split() + bd_name = parts[0] + self.data_dic[package_name]["Build-Depends"].append(bd) + if bd_name not in self.mapping.keys(): + self.dep_tree.add_edge(package_name, bd_name) + self.recursive_deps(bd_name, depth + 1) + elif self.mapping[bd_name] not in self.exists: + self.exists.append(self.mapping[bd_name]) + + pac_info = self.db.get_package_info(self.repo_name, package_name) + if (pac_info != None): + pre_depends = pac_info[2] + depends = pac_info[3] + + if (pre_depends != ""): + pre_depends_parser = DepParser(pre_depends) + for pd in pre_depends_parser.deps_and_version: + parts = pd.split() + pd_name = parts[0] + self.data_dic[package_name]["Pre-Depends"].append(pd) + if pd_name not in self.mapping.keys(): + self.dep_tree.add_edge(package_name, pd_name) + self.recursive_deps(pd_name, depth + 1) + elif self.mapping[pd_name] not in self.exists: + self.exists.append(self.mapping[pd_name]) + + if (depends != ""): + depends_parser = DepParser(depends) + for d in depends_parser.deps_and_version: + parts = d.split() + d_name = parts[0] + self.data_dic[package_name]["Depends"].append(d) + if d_name not in self.mapping.keys(): + self.dep_tree.add_edge(package_name, d_name) + self.recursive_deps(d_name, depth + 1) + elif self.mapping[d_name] not in self.exists: + self.exists.append(self.mapping[d_name]) + + def __str__(self): + return json.dumps(self.data_dic, indent=4, ensure_ascii=False) + + def write_into_json(self) -> None: + if self.json_path == "": + return + + with open(self.json_path, 'w') as file: + json.dump(self.data_dic, file, indent=4, ensure_ascii=False) + + def install_order(self) -> None: + """ + 拓扑排序给出的安装顺序 + """ + def get_items(items: List): + items_list = [] + for i in items: + if i == self.target: + continue + if i not in self.dep_tree.all_cycles.keys(): + items_list.append(i) + else: + items_list += get_items(self.dep_tree.all_cycles[i]) + + return items_list + + if self.target in self.mapping.keys(): + self.package_order = [self.mapping[self.target]] + + self.package_order = get_items(self.dep_tree.reversed_topological_sort()) + [self.target] + + def get_source_order(self): + """ + 根据安装顺序给出需要编译的源码包顺序及对应关系 + """ + for i in self.package_order[:-1]: + pac_info = self.db.get_package_info(self.repo_name, i) + if pac_info != None: + source_name = pac_info[-1] + if source_name not in self.source_and_package.keys(): + self.source_and_package[source_name] = [i] + else: + self.source_and_package[source_name].append(i) + + if source_name not in self.source_order: + self.source_order.append(source_name) + + +class DepTree: + def __init__(self, target:str, delete_cycles=True): + self.G = nx.DiGraph() + self.G.add_node(target) + self.delete_cycles = delete_cycles + self.cycle_cnt = 0 + self.node2cycle = {} + self.all_cycles = {} + + def add_edge(self, source, to): + while source in self.node2cycle.keys(): + source = self.node2cycle[source] + source_cycle = source + + while to in self.node2cycle.keys(): + to = self.node2cycle[to] + to_cycle = to + self.G.add_edge(source_cycle, to_cycle) + if self.delete_cycles: + self.process_cycles(source_cycle) + + def reversed_topological_sort(self) -> List: + return list(reversed(list(nx.topological_sort(self.G)))) + + def process_cycles(self, source) -> None: + """ + 将存在的环用新的节点替代,直至不存在环 + """ + try: + self.G.remove_edges_from(nx.selfloop_edges(self.G)) + cycles = list(nx.find_cycle(self.G, source, orientation='original')) + # cycles = [cyc for cyc in cycles_list if len(set(cyc[:-1])) > 1] + while (len(cycles) != 0): + new_node = f"cycle{self.cycle_cnt}" + self.all_cycles[new_node] = [] + cycle = [e[0] for e in cycles] + # print(f"处理环cycle{self.cycle_cnt}: {cycle}") + + # G_copy = self.G.copy() + + # 创建新的节点 + self.G.add_node(new_node) + + # 重定向所有指向环中节点的边到新节点 + for source in self.G.nodes(): + if source not in cycle: + for target in cycle: + if self.G.has_edge(source, target): + # self.G.remove_edge(source, target) + self.G.add_edge(source, new_node) + + # 重定向所有从环中节点发出的边到新节点 + for node in cycle: + neighbors = self.G.neighbors(node) + for target in neighbors: + if target not in cycle: + # self.G.remove_edge(node, target) + self.G.add_edge(new_node, target) + + for node in cycle: + # 删除原节点并记录 + self.node2cycle[node] = new_node + if node in self.all_cycles.keys(): + for n in self.all_cycles[node]: + self.all_cycles[new_node].append(n) + else: + self.all_cycles[new_node].append(node) + if node in self.all_cycles.keys(): + for target in self.all_cycles[node]: + self.node2cycle[target] = new_node + self.G.remove_node(node) + + self.cycle_cnt += 1 + self.G.remove_edges_from(nx.selfloop_edges(self.G)) + cycles = list(nx.find_cycle(self.G, new_node, orientation='original')) + + except nx.exception.NetworkXNoCycle: + return + + +if __name__ == "__main__": + assert len(sys.argv) == 2 + target = sys.argv[1] + + dd = DeepDep("main", target) + print(dd) + print(dd.exists) + print(dd.package_order) + print(dd.source_order) + # print(dd.dep_tree.all_cycles) + # print(dd.install_order()) + # for i in dd.install_order(): + # if i not in dd.dep_tree.all_cycles.keys(): + # print(i, end=" ") + # else: + # print(dd.dep_tree.all_cycles[i]) + diff --git a/tools/deb2rpm/scripts/spec.py b/tools/deb2rpm/scripts/spec.py new file mode 100644 index 0000000..833fc14 --- /dev/null +++ b/tools/deb2rpm/scripts/spec.py @@ -0,0 +1,314 @@ +import os +import sys +import re +import json +import logging +import shutil +import platform + +from datetime import datetime + +from scripts.deb_parser import DebParser +from scripts.get_files import DebFiles +from scripts.init_db import InitDB + +logger = logging.getLogger(__name__) + +home = os.getenv('HOME') + +json_path = f"{home}/deb2rpm/config/resources.json" +with open(json_path, 'r') as file: + json_data = json.load(file) +web_repo = json_data["repo_url"] + +info_map = { + 'Source': 'Name', + 'Homepage': 'URL', + 'Files': 'Source', + 'copyright': 'License', +} + +spec_head_data = ['Name', 'Epoch', 'Version', 'Release', 'License', 'URL'] + +def copy_directory_contents(source, destination): + for item in os.listdir(source): + if item == 'DEBIAN': + continue + + source_item = os.path.join(source, item) + destination_item = os.path.join(destination, item) + + if os.path.isdir(source_item): + if os.path.exists(destination_item): + if os.path.isdir(destination_item): + copy_directory_contents(source_item, destination_item) + else: + shutil.copytree(source_item, destination_item, symlinks=True) + elif os.path.isfile(source_item): + shutil.copy2(source_item, destination_item) + elif os.path.islink(source_item): + target_path = os.readlink(source_item) + os.symlink(target_path, destination_item) + +class Spec: + def __init__(self, path: str, build_path: str, repo_name: str, source_name: str, download_debs: bool): + """ + 初始化存储信息的Spec类 + @param path 执行apt source的目录(与DebParser相同) + @param build_path 源码包根目录(与DebFiles相同) + """ + self.meta_data = {} + self.files = {} + self.subdirs = {} + self.packages = {} + self.patches = [] + self.build_commands = [] + + # 解析dsc和control文件获取元数据信息 + deb_parser = DebParser(path, build_path) + for key, value in info_map.items(): + if key in deb_parser.meta_data.keys(): + self.meta_data[value] = deb_parser.meta_data[key] + + self.get_version(deb_parser.meta_data["Version"]) + + self.patches = deb_parser.patches + self.packages = deb_parser.control_data + + # 完成构建并获取文件信息 + deb_files = DebFiles(build_path, repo_name, source_name, download_debs) + self.files = deb_files.files + self.subdirs = deb_files.subdirs + self.build_commands = deb_files.commands + + def get_version(self, deb_version: str) -> None: + """ + 获取Epoch, Version, Release信息 + @param deb_version deb版本字符串 + """ + version_string = deb_version + epoch_pattern = r'^(\d+):' + release_pattern = r'(.*?)-(.*)' + + match_epoch = re.match(epoch_pattern, deb_version) + if match_epoch: + epoch = match_epoch.group(1) + self.meta_data['Epoch'] = epoch + version_string = version_string[len(epoch)+1:] + match_release = re.match(release_pattern, version_string) + if match_release: + release = match_release.group(2).strip() + version_string = version_string[:-len(release)-1] + self.meta_data["Release"] = '1' + self.meta_data["Version"] = version_string + +class SpecGenarator: + def __init__(self, repo_name: str, + target_name: str, + download_debs: bool, + rpm_source_path=f'{home}/rpmbuild/SOURCES', + target_path=f'{home}/rpmbuild/SPECS', + buildroot_path=f'{home}/rpmbuild/BUILDROOT'): + """ + 生成spec文件 + @param source_path 源码压缩包路径(与Spec类path相同) + @param repo_name 源名 + @param target_path 新建文件所属文件夹 + @param build_path 构建目录(与Spec类build_path相同) + @param buildroot_path buildroot路径 + """ + self.rpm_source_path = rpm_source_path + self.source_path = os.path.join(rpm_source_path, target_name) + self.target_path = target_path + self.buildroot_path = buildroot_path + self.db = InitDB() + + source_name, version, build_requires, dir_path, _, source_files = self.db.get_source_info(repo_name, target_name) + if os.path.exists(self.source_path): + shutil.rmtree(self.source_path) + logger.info(f"创建目录{self.source_path}") + os.mkdir(self.source_path) + + # 获取源文件 + os.chdir(self.source_path) + for file in source_files.split(): + logger.info(f"获取文件{file}") + os.system(f"wget {web_repo}/{dir_path}/{file}") + + # 生成build目录 + self.file_parts = source_files.split() + logger.info("生成build目录") + os.system(f"dpkg-source -x --no-check {self.file_parts[0]}") + + # 获取目录路径 + subdir = [os.path.join(self.source_path, x) for x in os.listdir(self.source_path) + if os.path.isdir(os.path.join(self.source_path, x))] + assert len(subdir) == 1, "源码包错误!" + + spec = Spec(self.source_path, subdir[0], repo_name, source_name, download_debs) + source_name = spec.meta_data['Name'] + spec_name = f"{source_name}-{spec.meta_data['Version']}.spec" + self.spec_file_path = os.path.join(target_path, spec_name) + with open(self.spec_file_path, 'w') as spec_file: + # 开头的元数据部分 + for key in spec_head_data: + if key in spec.meta_data.keys(): + spec_file.write(f'{key}: {spec.meta_data[key]}\n') + + if source_name in spec.packages.keys(): + spec_file.write(f"Summary: {spec.packages[source_name]['Summary']}\n") + else: + spec_file.write(f"Summary: the {source_name} source package\n") + + spec_file.write('\n') + spec_file.write('# BuildRequires\n') + + # Source和Patch字段 + spec_file.write('\n') + source_cnt = 0 + for source in spec.meta_data['Source']: + spec_file.write(f'Source{source_cnt}: {source}\n') + source_cnt += 1 + + patch_cnt = 0 + if len(spec.patches) != 0: + spec_file.write('\n') + for patch in spec.patches: + spec_file.write(f'Patch{patch_cnt}: {patch}\n') + patch_cnt += 1 + + spec_file.write('\n') + spec_file.write('%description\n') + if source_name in spec.packages.keys(): + spec_file.write(f"{spec.packages[source_name]['description']}\n") + else: + spec_file.write(f"the description of {source_name} is temporarily missing\n") + + # 子包元数据 + spec_file.write('\n') + for package_name, package_info_dir in spec.packages.items(): + if package_name == source_name: + continue + spec_file.write(f'%package {package_name}\n') + spec_file.write(f"Summary: {package_info_dir['Summary']}\n") + spec_file.write(f"# deps of {package_name}\n") + + spec_file.write(f'\n') + spec_file.write(f'%description {package_name}\n') + spec_file.write(f"{package_info_dir['description']}\n") + + spec_file.write('\n') + self.write_prep_commands(spec, spec_file, source_cnt, patch_cnt) + spec_file.write('\n') + self.write_build_commands(spec, spec_file) + spec_file.write('\n') + self.write_install_commands(spec, spec_file) + + if source_name in spec.files.keys(): + spec_file.write('\n') + spec_file.write('%files\n') + if len(spec.subdirs[source_name]) != 0: + for dir in spec.subdirs[source_name]: + spec_file.write(f'%dir /{dir}\n') + for file in spec.files[source_name]: + spec_file.write(f'/{file}\n') + + # 子包files + spec_file.write('\n') + for package_name, files in spec.files.items(): + if package_name == source_name: + continue + spec_file.write(f'%files {package_name}\n') + if len(spec.subdirs[package_name]) != 0: + for dir in spec.subdirs[package_name]: + spec_file.write(f'%dir /{dir}\n') + for file in files: + spec_file.write(f'/{file}\n') + spec_file.write('\n') + + # changelog + spec_file.write('\n') + spec_file.write('%changelog\n') + spec_file.write(f"* {datetime.now().strftime('%a %b %d %Y')} OpenEuler {spec.meta_data['Version']}-{spec.meta_data['Release']}\n") + spec_file.write(f'- Build {source_name} from deb to rpm\n') + + self.copy2source() + # shutil.rmtree(self.source_path) + + def write_prep_commands(self, spec: Spec, spec_file, source_cnt, patch_cnt): + """ + 填写%prep字段 + @param spec spec对象 + @param spec_file spec文件对象 + @param source_cnt Source文件个数 + @param patch_cnt Patch文件个数 + """ + assert source_cnt >= 2, "文件数应不少于2" + spec_file.write('%prep\n') + + spec_file.write('rm -rf %{_builddir}/%{name}-%{version}\n') + spec_file.write('mkdir %{_builddir}/%{name}-%{version}\n') + spec_file.write(f"tar -{self.extract_string(spec.meta_data['Source'][0])} %{{SOURCE0}} -C %{{_builddir}}/%{{name}}-%{{version}} --strip-components=1\n") + for i in range(1, source_cnt): + if spec.meta_data['Source'][i].endswith(".tar.gz") or spec.meta_data['Source'][i].endswith(".tar.xz") or spec.meta_data["Source"][i].endswith(".tar.bz2"): + spec_file.write(f"tar -{self.extract_string(spec.meta_data['Source'][i])} %{{SOURCE{i}}} -C %{{_builddir}}/%{{name}}-%{{version}}\n") + + spec_file.write('\n') + if patch_cnt > 0: + spec_file.write('cd %{_builddir}/%{name}-%{version}\n') + for i in range(patch_cnt): + spec_file.write(f"%patch{i} -p1\n") + + def extract_string(self, file_name: str) -> str: + """ + 根据文件名返回tar解压命令参数 + """ + if file_name.endswith(".tar.gz"): + return "xzf" + if file_name.endswith(".tar.xz"): + return "xJf" + if file_name.endswith(".tar.bz2"): + return "xjf" + + + def write_build_commands(self, spec: Spec, spec_file): + """ + 填写%build字段 + @param spec spec对象 + @param spec_file spec文件对象 + """ + spec_file.write('%build\n') + spec_file.write('cd %{name}-%{version}\n') + for command in spec.build_commands: + spec_file.write(f'fakeroot {command}\n') + + def write_install_commands(self, spec: Spec, spec_file): + """ + 填写%install字段 + @param spec spec对象 + @param spec_file spec文件对象 + """ + spec_file.write('%install\n') + spec_file.write('%define _unpackaged_files_terminate_build 0\n') + spec_file.write('rm -rf %{buildroot}/*\n') + for package_name in spec.packages.keys(): + spec_file.write(f"cp -af %{{_builddir}}/%{{name}}-%{{version}}/debian/{package_name}/* %{{buildroot}}\n") + + spec_file.write("rm -rf %{buildroot}/DEBIAN\n") + + def copy2source(self): + """ + 将所有文件复制到source根目录下 + """ + all_files = [os.path.join(self.source_path, x) for x in os.listdir(self.source_path) + if not os.path.isdir(os.path.join(self.source_path, x))] + for file in all_files: + shutil.copy(file, self.rpm_source_path) + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("One target!") + sys.exit() + + target = sys.argv[1] + SpecGenarator(target) diff --git a/tools/deb2rpm/scripts/sqlite_database.py b/tools/deb2rpm/scripts/sqlite_database.py new file mode 100644 index 0000000..4c2d119 --- /dev/null +++ b/tools/deb2rpm/scripts/sqlite_database.py @@ -0,0 +1,210 @@ +import sqlite3 +import os +import re + +from typing import List + +home = os.getenv('HOME') +db_name = f"{home}/deb2rpm/database/deb2rpm.db" + +def get_line_info(line: str) -> List: + """ + 从line中提取信息 + @param line 一行,格式为key: value + @return 返回[key, value] + """ + pattern = r'^(.*?):\s*(.*)' + match = re.search(pattern, line) + if match: + return match.group(1).strip(), match.group(2).strip() + return "", "" + +class BaseDB: + """ + 用于存储源文件名和依赖的数据库 + """ + def __init__(self): + self.conn = sqlite3.connect(db_name) + # print(f"成功连接数据库{db_name}") + self.cursor = self.conn.cursor() + + def create_table(self, table_name: str, keys: List, types: List): + """ + 如果名为table_name的表不存在,则创建表 + @param table_name 表名 + @param keys 键名的列表 + @param types 各个键对应的类型 + """ + columns = ', '.join([f'{key} {key_type}' for key, key_type in zip(keys, types)]) + self.cursor.execute(f'CREATE TABLE IF NOT EXISTS {table_name} ({columns})') + self.conn.commit() + + def insert_data(self, table_name: str, data: List): + """ + 向table_name中插入data(类型均为字符串型) + @param table_name 表名 + @param data 数据列表 + """ + datas = ', '.join([f"'{value}'" for value in data]) + self.cursor.execute(f'INSERT INTO {table_name} VALUES ({datas})') + self.conn.commit() + + def select_data(self, table_name: str, key: str, value: str, columns=None) -> List: + """ + 查询表,条件为key = value + @param table_name 表名 + @param key 条件中的键名 + @param value 条件中的值 + @param columns 要查询的列(默认为全部) + @return 符合条件的行列表 + """ + if columns: + select_columns = ', '.join(columns) + else: + select_columns = '*' + self.cursor.execute(f"SELECT {select_columns} FROM {table_name} WHERE {key} = '{value}'") + rows = self.cursor.fetchone() + return rows + + def delete_data(self, table_name:str, key: str, value: str): + """ + 删除满足key = value的数据 + @param table_name 表名 + @param key 条件中的键名 + @param value 条件中的值 + """ + self.cursor.execute(f"DELETE FROM {table_name} WHERE {key} = '{value}'") + self.commit() + + def table_exists(self, table_name: str) -> bool: + """ + 判断数据库中是否存在名为table_name的表 + @param table_name 表名 + @return 存在则返回True,不存在返回False + """ + self.cursor.execute(f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}'") + result = self.cursor.fetchone() + + if result: + return True + return False + + def drop_table(self, table_name: str): + """ + 删除表table_name + """ + self.cursor.execute(f'DROP TABLE IF EXISTS {table_name}') + self.conn.commit() + + +class SourceDB(BaseDB): + def __init__(self, table_name: str, path: str): + """ + 初始化source表 + @param table_name source表名 + @param path source文件路径 + """ + super().__init__() + self.table_name = table_name + self.keys = ['Package', 'Version', 'BuildDepends', 'Directory', 'Packages', 'Files'] + self.types = ['TinyText', 'TinyText', 'Text', 'TinyText', 'Text', 'Text'] + + super().create_table(table_name, self.keys, self.types) + get_files = False + get_packages = False + package_name = "" + version = "" + build_depends = "" + directory = "" + packages = "" + files = "" + with open(path, 'r') as file: + for line in file: + key, value = get_line_info(line) + if (line.strip() == "" or key == "Checksums-Sha1") and get_files: + get_files = False + super().insert_data(table_name, [package_name, version, build_depends.strip(',').strip(), directory, packages, files.strip()]) + package_name = "" + version = "" + build_depends = "" + directory = "" + packages = "" + files = "" + continue + if key == 'Package': + package_name = value + elif key == 'Version': + version = value + elif key == 'Package-List': + get_packages = True + elif key == 'Build-Depends': + build_depends = value + elif key == 'Directory': + directory = value + elif key == 'Build-Depends-Indep' or key == 'Build-Depends-Arch': + build_depends += f', {value}' + elif key == 'Files': + get_files = True + get_packages = False + elif get_files: + part = line.strip().split() + files += f" {part[-1]}" + elif get_packages: + part = line.strip().split() + packages += f" {part[0]}" + + self.conn.close() + + def select_data(self, key: str, value: str, columns=None) -> List: + super().select_data(self.table_name, key, value, columns) + + def close_db(self): + self.conn.close() + +class PackageDB(BaseDB): + def __init__(self, table_name: str, path: str): + """ + 初始化package表 + @param table_name package表名 + @param path source文件路径 + """ + super().__init__() + self.table_name = table_name + self.keys = ['Package','Provides', 'PreDepends','Depends','Recommends', 'Suggests', 'Breaks', 'Replaces', 'Source', 'Filename'] + self.types = ['TinyText', 'TinyText','TinyText', 'Text', 'TinyText', 'TinyText', 'TinyText', 'TinyText', 'TinyText', 'TinyText'] + super().create_table(table_name, self.keys, self.types) + + data = {} + for key in self.keys: + data[key] = "" + with open(path, 'r') as file: + for line in file: + key, value = get_line_info(line) + if key == 'Size': + if data['Source'] == '': + data['Source'] = data['Package'] + super().insert_data(table_name, data.values()) + data = {} + for key in self.keys: + data[key] = "" + continue + if key in self.keys: + data[key] = value + if key == 'Pre-Depends': + data['PreDepends'] = value + + self.conn.close() + + def select_data(self, key: str, value: str, columns=None) -> List: + super().select_data(self.table_name, key, value, columns) + + def close_db(self): + self.conn.close() + +if __name__ == "__main__": + source_db = SourceDB('SOURCE', '/home/young/Source/source_test') + package_db = PackageDB('PACKAGE', '/home/young/Package/package-test') + db = BaseDB() + res = db.select_data('PACKAGE', 'Package', 'nginx') + print(res) + # print(res[0]) -- Gitee