QPT-Family / QPT

[内测中]QPT - 致力于让开源项目更好通往互联网世界的Python to EXE工具(Python打包)。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[TWB] pdm 模式

SOVLOOKUP opened this issue · comments

Issue类型

建议 or 代码贡献请求

我的想法

pdm 是一个 python 包管理器,使项目拥有独立的依赖环境,并有非常清晰的依赖描述,例如:

[project]
name = "uie-api"
version = "0.0.1"
description = "paddlenlp uie web api"
authors = [
    {name = "sovlookup", email = "gonorth@qq.com"},
]
dependencies = [
    "paddlenlp>=2.3.3",
    "paddlepaddle>=2.3.0",
    "setuptools>=62.6.0",
    "fastapi>=0.78.0",
    "uvicorn[standard]>=0.18.2",
]
requires-python = ">=3.8"
license = {text = "MIT"}

一个 pdm 项目示例: https://github.com/SOVLOOKUP/uie-api

期待的回应

希望 qpt 能自动识别 pdm 项目,读取依赖及 python 环境等配置进行打包

Hi,开发者朋友,鉴于您是首次在QPT下新增Issue,请务必参考给定的Issue模板进行Issue的新增。
虽然形如版本号、打包日志等信息可能对你并不重要,但如未提供则可能会严重拖慢Issue的解决效率,因为他们对于QPT的维护者而言非常重要!
如您未按Issue模板进行内容补充,可关闭此Issue或Edit该Issue,社区维护者会对低效Issue直接Close,请悉知!

加入Todo ,等有空就学习一下哈哈哈

如果能给我一些 tips (qpt 是如何进行依赖扫描的;如果我要改变扫描逻辑应该修改哪个文件),我可以帮助完成这个功能

这部分代码写的有点乱,各种写死打补丁哈哈哈(想重构也是可以的,最近实在有点忙)
主要分为两个大部分:

依赖检索

1.用户代码依赖检索

由于要判断哪些依赖需要被打包,所以要对用户代码进行检索,检索import的情况,根据import情况来判断用户可能用到了哪些依赖(绝大部分是可以这样检索得到)。
但这样也面临一个问题:import的是文件夹,而非真正的依赖名,例如opencv-python>=2.0在import时是import cv2,以及paddlepaddle在import时是import paddle,那么我们还需要对用户环境中的依赖情况检索才能判断import的文件夹名对应的是哪个依赖包,以及依赖包的版本。

该部分代码位于:https://github.com/QPT-Family/QPT/blob/%E5%BC%80%E5%8F%91%E5%88%86%E6%94%AF/qpt/kernel/qcode.py

2.用户环境依赖检索

由于使用pip、setuptools安装的依赖包,默认会在site_packages目录中给一个依赖包名_版本号_dist目录,这个目录下有Top_level.txt文件,文件中会记录该依赖对应的import名,通过对该文件的检索即可拿到import与依赖包名的关系。

同样,依赖包名_版本号_dist目录可以给我们很多信息,包括依赖的依赖情况(例如paddlepaddle依赖numpy、pillow),这部分可以通过pip来做检索(实际上我没玩透这个,而且pip更新很不向下兼容,以至于QPT要严格现在pip版本号)

该部分代码位于:

# Author: Acer Zhang

当前已知的Bug(Anaconda中部分包丢失Top_level.txt信息):#65

3.依赖匹配

在做完两部分检索后,就要进行依赖的匹配,生成一个requirement文件,包含主依赖+对应版本号+被注释掉的主依赖的依赖(pip会默认忽略,所以这里也默认注释了)

依赖封装

解析requirement

因为客户机不一定会安装VS2019这样的开发环境,所以例如lap这样原作者没有提供whl包的依赖,可能需要用户具备编译环境。未来更好让用户去控制依赖如何被封装,这里的requirement文件就多了一些特殊标识符(QPT目前还没弄太好) :

QPT_DISPLAY_FLAG = "#$QPT_FLAG$"
DISPLAY_IGNORE = "ignore"
DISPLAY_COPY = "copy" # 复制开发环境编译好的文件 - 文件列表也可以从`依赖包名_版本号_dist`目录里的RECORD或者installed-files.txt文件中找到
DISPLAY_FORCE = "force"  # 独立安装
DISPLAY_NET_INSTALL = "net_install"  # 打包时从第三方站点下载,部署时安装
DISPLAY_ONLINE_INSTALL = "online_install"  # 在线安装
DISPLAY_LOCAL_INSTALL = "local_install"  # 本地下载后安装
DISPLAY_SETUP_INSTALL = "setup_install"  # 直接安装

例如我们想避免在客户机上编译安装lap,那么requirement里就可以写为:
lap==xxx #$QPT_FLAG$copy

代码解析部分:

def analyze_requirements_file(file_path):

获取文件列表:

def get_package_all_file(package, site_package_path=None):

封装为SubModule

这部分是最烂的地方,太想重构了(

封装用于Requirement文件安装的SubModule例子:https://github.com/QPT-Family/QPT/blob/26723288a15efbc4627e7cfcff6443ec5460418b/qpt/modules/auto_requirements.py

实际上就是对CustomPackage这个类的批量实例化,CustomPackage实现代码如下:

class CustomPackage(SubModule):

Tips:
一个SubModule是由N个SubModuleOpt组成的,SubModuleOpt是最小执行单元,可以通过重写这个类的act方法来实现开发环境or用户环境要执行的操作(shell、pip等)
例如我们想封装一个下载whl文件的SubModuleOpt:https://github.com/QPT-Family/QPT/blob/%E5%BC%80%E5%8F%91%E5%88%86%E6%94%AF/qpt/modules/package.py#L44

然后封装在SubModule:https://github.com/QPT-Family/QPT/blob/%E5%BC%80%E5%8F%91%E5%88%86%E6%94%AF/qpt/modules/package.py#L201
-> self.add_pack_opt开发环境执行 self.add_unpack_opt客户环境执行

最后,加入到封装流程中:https://github.com/QPT-Family/QPT/blob/%E5%BC%80%E5%8F%91%E5%88%86%E6%94%AF/examples/advanced/%E4%BD%BF%E7%94%A8SubModule%E6%B7%BB%E5%8A%A0%E8%87%AA%E5%AE%9A%E4%B9%89whl.md

SubModule最开始设计时觉得能灵活解决很多问题了,但现在发现只有增加,并没有删除这个操作,可能后续还会再改进一下,基类源码部分在:https://github.com/QPT-Family/QPT/blob/%E5%BC%80%E5%8F%91%E5%88%86%E6%94%AF/qpt/modules/base.py

(请勿在SubModule中使用module_path等成员变量,因为这些变量只是占位,只有SubModuleOpt部分才会真实有效,这部分文档有空一定更新[狗头])

已经发布啦~感谢贡献