diff --git a/README.MD b/README.MD index ae2bfe0..3325cf8 100644 --- a/README.MD +++ b/README.MD @@ -19,7 +19,7 @@ EAP邀测版本:当前版本为尝鲜版本,仅具备基本功能,而且 ## 快速使用QPT #### 安装QPT到当前环境 -> 当前版本号为 1.0a4 +> 当前版本号为 1.0a* `pip install qpt -i https://pypi.tuna.tsinghua.edu.cn/simple -U` or `pip install qpt -U` diff --git a/ToDo.MD b/ToDo.MD new file mode 100644 index 0000000..50819ab --- /dev/null +++ b/ToDo.MD @@ -0,0 +1,14 @@ +# 细节性ToDO + +## 高优优先级 +- [ ] 针对Python37暴露的兼容性问题 +- [ ] 针对Flask暴露的依赖问题 +- [ ] 对邀测用户进行本地适配 +- [ ] 增加CUDA支持 +## 一般优先级 +- [ ] 重写依赖搜索 +- [ ] 杀进程 +- [ ] 控制台程序 +- [ ] 排除VENV文件夹 +- [ ] 要不要加个环境变量PYTHONPATH +- [ ] 图标问题可参考 \ No newline at end of file diff --git a/examples/paddle_sandbox/paddle_program/run.py b/examples/paddle_sandbox/paddle_program/run.py index a65f3bc..24aca7a 100644 --- a/examples/paddle_sandbox/paddle_program/run.py +++ b/examples/paddle_sandbox/paddle_program/run.py @@ -25,7 +25,7 @@ def __init__(self): QMessageBox.Yes) -app = QApplication(sys.argv) -ex = Example() - +if __name__ == '__main__': + app = QApplication(sys.argv) + ex = Example() input("PaddlePaddle测试成功!") diff --git a/qpt/executor.py b/qpt/executor.py index 801be94..f832cd0 100644 --- a/qpt/executor.py +++ b/qpt/executor.py @@ -11,10 +11,12 @@ from qpt._compatibility import com_configs from qpt.modules.base import SubModule from qpt.modules.python_env import BasePythonEnv, AutoPythonEnv -from qpt.modules.package import AutoRequirementsPackage, QPTDependencyPackage, DEFAULT_DEPLOY_MODE +from qpt.modules.package import AutoRequirementsPackage, QPTDependencyPackage, DEFAULT_DEPLOY_MODE, \ + set_default_deploy_mode, BatchInstallation from qpt.kernel.tools.log_op import Logging -from qpt.kernel.tools.os_op import clean_qpt_cache +from qpt.kernel.tools.os_op import clean_qpt_cache, copytree +from qpt.kernel.tools.terminal import AutoTerminal class CreateExecutableModule: @@ -22,6 +24,7 @@ def __init__(self, work_dir, launcher_py_path, save_path, + ignore_dirs: list = None, requirements_file="auto", deploy_mode=DEFAULT_DEPLOY_MODE, sub_modules: List[SubModule] = None, @@ -32,13 +35,20 @@ def __init__(self, none_gui: bool = False, with_debug: bool = True): # 初始化路径成员变量 - self.launcher_py_path = os.path.abspath(launcher_py_path).replace(os.path.abspath(work_dir) + "\\", "./") + self.launcher_py_path = os.path.abspath(launcher_py_path).replace(os.path.abspath(work_dir), "") + if self.launcher_py_path[:1] == "\\": + self.launcher_py_path = self.launcher_py_path[1:] self.work_dir = work_dir + assert os.path.abspath(work_dir) not in os.path.abspath(save_path), \ + "打包后的保存路径不能在被打包的文件夹中,这样会打包了打包后的文件^n (,,•́ . •̀,,)" self.save_path = save_path self.module_path = os.path.join(save_path, "Release") self.debug_path = os.path.join(save_path, "Debug") self.interpreter_path = os.path.join(self.module_path, "Python") + if ignore_dirs is None: + self.ignore_dirs = list() + set_default_deploy_mode(deploy_mode) self.with_debug = with_debug # 新建配置信息 @@ -63,6 +73,12 @@ def __init__(self, # 设置全局下载的Python包默认解释器版本号 - 更换兼容性方案 # set_default_package_for_python_version(interpreter_module.python_version) + # 避免打包虚拟环境 + for root, dirs, files in os.walk(self.work_dir): + if files == "pyvenv.cfg": + self.ignore_dirs.append(root) + Logging.warning(f"检测到{files},推测出{root}为Python虚拟环境主目录,在打包时会忽略该目录") + # 获取SubModule列表 self.lazy_module = [interpreter_module, QPTDependencyPackage()] @@ -70,6 +86,9 @@ def __init__(self, pass self.sub_module = sub_modules if sub_modules is not None else list() + # 放入增强包 + self.sub_module.append(BatchInstallation()) + # 解析依赖 if requirements_file == "auto": auto_dependency_module = AutoRequirementsPackage(path=self.work_dir, @@ -105,10 +124,9 @@ def _solve_module(self, lazy=False): # lazy mode 不支持terminal terminal = None else: - from qpt.kernel.tools.qpt_qt import QTerminal, MessageBoxTerminalCallback - self.terminal = QTerminal() + self.terminal = AutoTerminal() modules = self.sub_module - terminal = self.terminal.shell_func(callback=MessageBoxTerminalCallback()) + terminal = self.terminal.shell_func() # 依靠优先级进行排序 modules.sort(key=lambda m: m.level, reverse=True) for sub in modules: @@ -144,8 +162,20 @@ def make(self): # 复制资源文件 assert os.path.exists(self.work_dir), f"{os.path.abspath(self.work_dir)}不存在,请检查该路径是否正确" - shutil.copytree(self.work_dir, self.resources_path) - + copytree(self.work_dir, self.resources_path, ignore_dirs=self.ignore_dirs) + + # 避免出现if __name__ == '__main__': + with open(os.path.join(self.resources_path, self.launcher_py_path), "r", encoding="utf-8") as lf: + lf_codes = lf.readlines() + for lf_code_id, lf_code in enumerate(lf_codes): + if "if" in lf_code and "__name__" in lf_code and "__main__" in lf_code: + # 懒得写正则了嘿嘿嘿 + Logging.warning(f"{self.launcher_py_path}中包含if __name__ == '__main__'语句," + f"由于用户使用时QPT成为了主程序,故此处代码块会被Python忽略。" + f"为保证可以正常执行,当前已自动修复该问题") + with open(os.path.join(self.resources_path, self.launcher_py_path), "w", encoding="utf-8") as new_lf: + lf_codes[lf_code_id] = lf_code[:lf_code.index("if ")] + "if 'qpt':\n" + new_lf.writelines(lf_codes) # 创建配置文件 os.makedirs(self.config_path, exist_ok=True) with open(self.config_file_path, "w", encoding="utf-8") as config_file: @@ -154,31 +184,22 @@ def make(self): # 复制Debug所需文件 if self.with_debug: debug_dir = os.path.join(os.path.split(qpt.__file__)[0], "ext/launcher_debug") - try: - shutil.copytree(debug_dir, dst=self.debug_path, dirs_exist_ok=True) - shutil.copytree(self.module_path, dst=self.debug_path, dirs_exist_ok=True) - except TypeError: - shutil.copytree(debug_dir, dst=self.debug_path) - shutil.copytree(self.module_path, dst=self.debug_path) - Logging.debug("打包策略将对Python3.7版本进行支持") - finally: - # 生成Debug标识符 - unlock_file_path = os.path.join(self.debug_path, "configs/unlock.cache") - with open(unlock_file_path, "w", encoding="utf-8") as unlock_file: - unlock_file.write(str(datetime.datetime.now())) - - # 重命名兼容模式文件 - compatibility_mode_file = os.path.join(self.debug_path, "compatibility_mode.cmd") - if os.path.exists(compatibility_mode_file): - os.rename(compatibility_mode_file, - os.path.join(self.debug_path, "使用兼容模式运行.cmd")) + copytree(debug_dir, dst=self.debug_path) + copytree(self.module_path, dst=self.debug_path) + # 生成Debug标识符 + unlock_file_path = os.path.join(self.debug_path, "configs/unlock.cache") + with open(unlock_file_path, "w", encoding="utf-8") as unlock_file: + unlock_file.write(str(datetime.datetime.now())) + + # 重命名兼容模式文件 + compatibility_mode_file = os.path.join(self.debug_path, "compatibility_mode.cmd") + if os.path.exists(compatibility_mode_file): + os.rename(compatibility_mode_file, + os.path.join(self.debug_path, "使用兼容模式运行.cmd")) # 复制Release启动器文件 launcher_dir = os.path.join(os.path.split(qpt.__file__)[0], "ext/launcher") - try: - shutil.copytree(launcher_dir, dst=self.module_path, dirs_exist_ok=True) - except TypeError: - shutil.copytree(launcher_dir, dst=self.module_path) + copytree(launcher_dir, dst=self.module_path) # 重命名兼容模式文件 compatibility_mode_file = os.path.join(self.module_path, "compatibility_mode.cmd") if os.path.exists(compatibility_mode_file): @@ -207,8 +228,10 @@ def make(self): f"| 文件或重新打包,以避免因执行“启动程序.exe”后丢失“一次性部署模块”,从而无法被他人使用。\n" f"| ----------------------------------------------------------------------------- |\n") + sys.stdout.flush() Logging.info("是否需要保留QPT在打包时产生的缓存文件?若不清空则可能会在下次使用QPT时复用缓存以提升打包速度") clear_key = input("[保留(Y)/清空(N)]:_") + sys.stdout.flush() if clear_key.lower() == "n": clean_qpt_cache() Logging.info("QPT缓存已全部清空") @@ -240,12 +263,11 @@ def solve_qpt_env(self): def _solve_module(self): modules = self.lazy_module + self.sub_module - from qpt.kernel.tools.qpt_qt import QTerminal, MessageBoxTerminalCallback from qpt.gui.qpt_unzip import Unzip from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QIcon - self.terminal = QTerminal() - terminal = self.terminal.shell_func(callback=MessageBoxTerminalCallback()) + self.terminal = AutoTerminal() + terminal = self.terminal.shell_func() app = QApplication(sys.argv) unzip_bar = Unzip() unzip_bar.setWindowIcon(QIcon(os.path.join(self.base_dir, "Logo.ico"))) @@ -257,7 +279,7 @@ def _solve_module(self): module_path=self.base_dir, terminal=terminal) sub_module.unpack() - unzip_bar.update_value(sub_module_id / len(self.sub_module) * 100) + unzip_bar.update_value(min(sub_module_id / len(self.sub_module) * 100, 99)) unzip_bar.update_title(f"正在初始化:{sub_name}") app.processEvents() @@ -309,8 +331,8 @@ def run(self): self.solve_work_dir() # 执行主程序 main_lib_path = self.configs["launcher_py_path"].replace(".py", "") - assert "." not in main_lib_path[1:], "封装Module时需要避免路径中带有'.'字符,该字符将影响执行程序" - main_lib_path = main_lib_path[2:]. \ + assert "." not in main_lib_path, "需要避免路径中带有'.'字符,该字符将严重影响程序执行" + main_lib_path = main_lib_path. \ replace(".py", ""). \ replace(r"\\", "."). \ replace("\\", "."). \ diff --git a/qpt/gui/GUI_module_launcher.py b/qpt/gui/GUI_module_launcher.py index b54e716..dd02f9c 100644 --- a/qpt/gui/GUI_module_launcher.py +++ b/qpt/gui/GUI_module_launcher.py @@ -6,7 +6,7 @@ from qpt.kernel.qt_ui import GUIML from qpt.kernel.tools.log_op import Logging -from qpt.kernel.tools.qpt_qt import QTCondaVenv, QTerminal +from qpt.kernel.tools.terminal import QTCondaVenv, QTerminal from qpt.kernel.tools.os_op import SysInfo """ diff --git a/qpt/kernel/tools/os_op.py b/qpt/kernel/tools/os_op.py index 459389f..9df89e9 100644 --- a/qpt/kernel/tools/os_op.py +++ b/qpt/kernel/tools/os_op.py @@ -94,6 +94,35 @@ def flush(self): self.buff = '' +def copytree(src, dst, ignore_dirs: list = None): + """ + 复制整个目录树 + 最开始是用shutil.copytree(),但奈何Python3.7和3.8差别挺大,算了忽略这点效率吧,反正是在打包过程中,不影响用户 + :param src: 源路径 + :param dst: 目标路径 + :param ignore_dirs: 忽略的文件夹名 + """ + if ignore_dirs is None: + ignore_dirs = list() + else: + ignore_dirs = [os.path.abspath(d) for d in ignore_dirs] + if not os.path.exists(dst): + os.makedirs(dst) + + if os.path.exists(src): + for root, dirs, files in os.walk(src): + if os.path.abspath(root) in ignore_dirs: + continue + dst_root = os.path.join(os.path.abspath(dst), + os.path.abspath(root).replace(os.path.abspath(src), "").strip("\\")) + for file in files: + src_file = os.path.join(root, file) + dst_file = os.path.join(dst_root, file) + if not os.path.exists(dst_root): + os.makedirs(dst_root, exist_ok=True) + shutil.copy(src_file, dst_file) + + class FileSerialize: def __init__(self, file_path): with open(file_path, "r", encoding="utf-8")as file: diff --git a/qpt/kernel/tools/qpt_qt.py b/qpt/kernel/tools/qpt_qt.py deleted file mode 100644 index d22ea10..0000000 --- a/qpt/kernel/tools/qpt_qt.py +++ /dev/null @@ -1,231 +0,0 @@ -import PyQt5 -from PyQt5 import QtCore -from PyQt5.QtCore import pyqtSignal - -from qpt.kernel.tools.log_op import Logging - -TERMINAL_NAME = "cmd.exe" - - -class TerminalCallback: - def __init__(self): - pass - - def normal_func(self): - pass - - def error_func(self): - pass - - @staticmethod - def _req_qt_process_normal_info(terminal): - result = terminal.readAllStandardOutput().data().decode("gbk") - return result - - @staticmethod - def _req_qt_process_error_info(terminal): - result = terminal.readAllStandardError().data().decode("gbk") - return result - - -class MessageBoxTerminalCallback(TerminalCallback): - pass - - -class QTerminal: - def __init__(self): - # 终端占位 - self.main_terminal = None - # 信息输出绑定函数占位 - self.terminal_normal_info_func = None - self.terminal_error_info_func = None - - self.init_terminal() - - def init_terminal(self): - self.main_terminal = QtCore.QProcess() - self.main_terminal.start(TERMINAL_NAME) - - def reset_terminal(self): - self.main_terminal.close() - self.init_terminal() - - def close_terminal(self): - self.main_terminal.close() - - def shell(self, shell, callback: TerminalCallback): - self.shell_func(callback)(shell) - - def shell_func(self, callback: TerminalCallback): - if callback: - self.main_terminal.readyReadStandardOutput.connect(callback.normal_func) - self.main_terminal.readyReadStandardError.connect(callback.error_func) - - def closure(closure_shell): - closure_shell += "&&echo GACT:DONE!||echo GACT:ERROR!\n" - # 发送指令 - self.main_terminal.write(closure_shell.encode("gbk")) - - return closure - -# class TerminalO(PyQt5.QtCore.QThread): -# """ -# QT的Terminal管理类,收到指令后会获取输出结果,返回正确与异常结果并给出状态 -# """ -# # 初始化信号 -# # shell_final_signal = pyqtSignal(str) -# shell_done_signal = pyqtSignal(str) -# shell_error_signal = pyqtSignal(str) -# -# def __init__(self, q_process=None, text_browser=None): -# """ -# :param text_browser: 可供输出的text_browser、label等QT组件,若为None则直接返回结果 -# :param q_process: -# """ -# super().__init__() -# self.terminal_text_browser = text_browser -# -# # 初始化状态 -# self.init_act = False -# # 初始化执行后决策函数 -# self.done_opt = None -# self.error_opt = None -# # 当有ERROR时控制只弹出ERROR消息框, False为没有ERROR -# self.policy_flag = False -# -# if q_process is None: -# self.shell_init_func() -# else: -# self.main_terminal = q_process -# -# def set_act_opt(self, done_opt=None, no_opt=None): -# """ -# 设置send_shell中opt操作 -# """ -# if self.done_opt is not None: -# self.shell_done_signal.disconnect(self.done_opt) -# if self.error_opt is not None: -# self.shell_error_signal.disconnect(self.error_opt) -# if done_opt: -# self.done_opt = done_opt -# self.shell_done_signal.connect(self.done_opt) -# if no_opt: -# self.error_opt = no_opt -# self.shell_error_signal.connect(self.error_opt) -# -# def send_shell(self, shell, done_opt=None, no_opt=None): -# """ -# 发送shell指令 -# :param shell:shell指令 -# :param done_opt: 运行成功则执行yes_opt() -# :param no_opt: 运行失败则执行no_opt() -# """ -# # !待完善 可以采用信号+序列来依次执行(或许在set时候直接组队列?),避免多个指令同事传输导致执行反馈为最后一次设置 -# self.set_act_opt(done_opt, no_opt) -# # 当有ERROR时控制只弹出ERROR消息框, False为没有ERROR -# self.policy_flag = False -# # 模拟按下回车并通过&&echo GACT:DONE!||echo GACT:ERROR!来判断执行是否正常,暂时还没想出更好的方案 -# shell += "&&echo GACT:DONE!||echo GACT:ERROR!\n" -# -# # 在GUI展示发送的命令 -# if self.terminal_text_browser is not None: -# self.write_text(f"{'-' * 50}") -# self.write_text( -# f"[Input]>>> {shell} ") -# self.write_text(f"{'-' * 50}") -# -# # 发送指令 -# self.main_terminal.write(shell.encode("gbk")) -# -# def shell_init_func(self): -# """ -# 初始化终端进程 -# """ -# if self.init_act is False: -# self.main_terminal = QtCore.QProcess() -# # 绑定输出 -# self.main_terminal.readyReadStandardOutput.connect(self.req_terminal_txt) -# self.main_terminal.readyReadStandardError.connect(self.req_terminal_error_txt) -# # !需完善-兼容性仅Windows -# self.main_terminal.start(TERMINAL_NAME) -# self.write_text("-----控制台链接成功!----- " -# "") -# self.init_act = True -# else: -# self.main_terminal.start(TERMINAL_NAME) -# -# def shell_reset_func(self): -# """ -# 重新启动终端 -# """ -# self.main_terminal.close() -# self.shell_init_func() -# self.write_text("终端已重启") -# -# def shell_clear_func(self): -# self.terminal_text_browser.clear() -# self.write_text("-----控制台信息清空成功!----- " -# "") -# -# def set_text_browser(self, text_browser): -# """ -# 设置输出容器 -# """ -# self.terminal_text_browser = text_browser -# -# def write_text(self, text: str, in_error=False): -# """ -# 向GUI的终端界面中添加文本 -# :param text: 需要添加的文本 -# :param in_error:来自ERROR的数据 -# """ -# if self.terminal_text_browser is None: -# return None -# text = text.strip("\n").replace("&&echo GACT:DONE!||echo GACT:ERROR!", "") -# self.terminal_text_browser.append("" + text) -# # 对执行结果进行评估,进行决策 -# if self.done_opt and not self.policy_flag: -# if "GACT:DONE!" in text and self.done_opt and not in_error: -# self.shell_done_signal.emit("执行完毕") -# if ("GACT:ERROR!" in text and self.error_opt) or in_error: -# self.policy_flag = True -# self.shell_error_signal.emit("部分报错信息:" + text) -# -# # 持续滚动,保证显示是最新一行 -# cursor = self.terminal_text_browser.textCursor() -# pos = len(self.terminal_text_browser.toPlainText()) -# cursor.setPosition(pos) -# self.terminal_text_browser.setTextCursor(cursor) -# -# def req_terminal_error_txt(self): -# """ -# 获取终端报错输出并添加到GUI界面中 -# """ -# result = self.main_terminal.readAllStandardError().data().decode("gbk") -# # 不是很清楚为什么我的电脑在打开cmd时候会出现这个,先屏蔽掉好了 -# if "系统找不到指定的路径。" in result: -# return "系统找不到指定的路径。" -# if result: -# Logging.debug(result) -# # 返回消息 -# if self.terminal_text_browser is None: -# return result -# else: -# self.write_text(f"[Input]>>>[可能发生了异常]{result} " -# f"", -# in_error=True) -# -# def req_terminal_txt(self): -# """ -# 获取终端输出并添加到GUI界面中 -# """ -# result = self.main_terminal.readAllStandardOutput().data().decode("gbk") -# if result: -# # 返回消息 -# Logging.debug(result) -# if self.terminal_text_browser is None: -# return result -# self.write_text(result) -# -# def req_terminal(self): -# return self.main_terminal diff --git a/qpt/kernel/tools/terminal.py b/qpt/kernel/tools/terminal.py new file mode 100644 index 0000000..94a9554 --- /dev/null +++ b/qpt/kernel/tools/terminal.py @@ -0,0 +1,183 @@ +import subprocess +from qpt.kernel.tools.log_op import Logging + +TERMINAL_NAME = "cmd.exe" +TERMINAL_MSG_FITTER_TAG = ["Microsoft Windows [版本", "(c) Microsoft Corporation。保留所有权利。"] +SHELL_ACT = "&&echo GACT:DONE!||echo GACT:ERROR!\n" + +try: + import PyQt5 + from PyQt5 import QtCore + from PyQt5.QtCore import pyqtSignal + + TERMINAL_TYPE = "QTerminal" +except ModuleNotFoundError: + TERMINAL_TYPE = "PTerminal" + Logging.debug("当前环境不存在PyQT") + + +class TerminalCallback: + def __init__(self): + pass + + def handle(self, terminal=None): + """ + 该函数将获取终端输出,并且需要显式调用执行成功与失败的func + """ + raise NotImplementedError(f"{self.__class__.__name__}中未定义handle方法") + + def normal_func(self): + """ + 终端执行成功后需要执行的Method + """ + raise NotImplementedError(f"{self.__class__.__name__}中未定义normal_func方法") + + def error_func(self): + """ + 终端执行失败后需要执行的Method + """ + raise NotImplementedError(f"{self.__class__.__name__}中未定义error_func方法") + + +class MessageBoxTerminalCallback(TerminalCallback): + def handle(self, terminal=None): + pass + + def normal_func(self): + pass + + def error_func(self): + pass + + +class LoggingTerminalCallback(TerminalCallback): + def handle(self, terminal=None): + assert terminal, "此处需要terminal" + line = True + while line: + line = terminal.stdout.readline() + if line == b'GACT:DONE!\r\n': + self.normal_func() + break + elif line == b'GACT:ERROR!\r\n': + self.error_func() + break + msg = line.decode('gbk').strip("b'").strip("\n") + if msg == "\r": + continue + + for tag_id, tag in enumerate(TERMINAL_MSG_FITTER_TAG): + if tag in msg: + break + if tag_id == len(TERMINAL_MSG_FITTER_TAG) - 1: + Logging.debug(msg) + + def normal_func(self): + Logging.debug("终端命令执行成功!") + + def error_func(self): + Logging.error("终端命令执行失败!") + + +class Terminal: + """ + Terminal基类,定义Terminal基本方法 + """ + + def __init__(self): + self.main_terminal = None + self.init_terminal() + Logging.debug(f"正在连接{self.__class__.__name__}") + + def init_terminal(self): + raise NotImplementedError(f"{self.__class__.__name__}中未定义init_terminal方法") + + def reset_terminal(self): + raise NotImplementedError(f"{self.__class__.__name__}中未定义reset_terminal方法") + + def close_terminal(self): + raise NotImplementedError(f"{self.__class__.__name__}中未定义close_terminal方法") + + def shell(self, shell, callback: TerminalCallback = None): + raise NotImplementedError(f"{self.__class__.__name__}中未定义shell方法") + + def shell_func(self, callback: TerminalCallback = None): + raise NotImplementedError(f"{self.__class__.__name__}中未定义shell_func方法") + + +class PTerminal(Terminal): + def __init__(self): + super(PTerminal, self).__init__() + + def init_terminal(self): + self.main_terminal = subprocess.Popen(TERMINAL_NAME, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE, + shell=True) + + def reset_terminal(self): + self.close_terminal() + self.init_terminal() + + def close_terminal(self): + self.main_terminal.terminate() + + def shell(self, shell, callback: TerminalCallback = LoggingTerminalCallback()): + self.shell_func(callback)(shell) + + def shell_func(self, callback: TerminalCallback = LoggingTerminalCallback()): + # ToDo 实现Callback + def closure(closure_shell): + Logging.debug(f"SHELL: {closure_shell}") + closure_shell += "&&echo GACT:DONE!||echo GACT:ERROR!\n" + # 发送指令 + self.main_terminal.stdin.write(closure_shell.encode("gbk")) + self.main_terminal.stdin.flush() + callback.handle(self.main_terminal) + + return closure + + +class QTerminal(Terminal): + def __init__(self): + super(QTerminal, self).__init__() + # 信息输出绑定函数占位 + self.terminal_normal_info_func = None + self.terminal_error_info_func = None + + def init_terminal(self): + self.main_terminal = QtCore.QProcess() + self.main_terminal.start(TERMINAL_NAME) + + def reset_terminal(self): + self.close_terminal() + self.init_terminal() + + def close_terminal(self): + self.main_terminal.close() + + def shell(self, shell, callback: TerminalCallback = MessageBoxTerminalCallback()): + self.shell_func(callback)(shell) + + def shell_func(self, callback: TerminalCallback = MessageBoxTerminalCallback()): + self.main_terminal.readyReadStandardOutput.connect(callback.normal_func) + self.main_terminal.readyReadStandardError.connect(callback.error_func) + + def closure(closure_shell): + Logging.debug(f"SHELL: {closure_shell}") + closure_shell += "&&echo GACT:DONE!||echo GACT:ERROR!\n" + # 发送指令 + self.main_terminal.write(closure_shell.encode("gbk")) + callback.handle() + + return closure + + +# 实现自动Terminal类型 +# AutoTerminal = PTerminal if TERMINAL_TYPE == "PTerminal" else QTerminal + +AutoTerminal = PTerminal # 当前正在测试QTerminal,暂时只使用PTerminal +if __name__ == '__main__': + t = AutoTerminal() + t.shell("ping 192.168.1.1") diff --git a/qpt/modules/package.py b/qpt/modules/package.py index d3b6800..4b4e5fb 100644 --- a/qpt/modules/package.py +++ b/qpt/modules/package.py @@ -5,12 +5,14 @@ import os -from qpt.modules.base import SubModule, SubModuleOpt, HIGH_LEVEL, TOP_LEVEL_REDUCE, GENERAL_LEVEL_REDUCE +from qpt.modules.base import SubModule, SubModuleOpt, HIGH_LEVEL, TOP_LEVEL_REDUCE, GENERAL_LEVEL_REDUCE, LOW_LEVEL from qpt.kernel.tools.log_op import Logging from qpt.kernel.tools.interpreter import PipTools from qpt.kernel.tools.os_op import get_qpt_tmp_path, FileSerialize from qpt._compatibility import com_configs +DOWN_PACKAGES_RELATIVE_PATH = "opt/packages" + pip = PipTools() @@ -26,7 +28,7 @@ def set_pip_tools(lib_package_path=None, # 第三方库部署方式 -LOCAL_DOWNLOAD_DEPLOY_MODE = "为用户准备Whl包,首次启动时会自动安装,可能会有兼容性问题" +LOCAL_DOWNLOAD_DEPLOY_MODE = "为用户准备Whl包,首次启动时会自动安装,即使这样也可能会有兼容性问题" LOCAL_INSTALL_DEPLOY_MODE = "[不推荐]预编译第三方库,首次启动无需安装但将额外消耗硬盘空间,可能会有兼容性问题并且只支持二进制包" ONLINE_DEPLOY_MODE = "用户使用时在线安装Python第三方库" DEFAULT_DEPLOY_MODE = LOCAL_DOWNLOAD_DEPLOY_MODE @@ -76,7 +78,7 @@ def act(self) -> None: self.package = "-r " + FileSerialize.serialize2file(self.package) pip.download_package(self.package, version=self.version, - save_path=os.path.join(self.module_path, "opt/packages"), + save_path=os.path.join(self.module_path, DOWN_PACKAGES_RELATIVE_PATH), no_dependent=self.no_dependent, find_links=self.find_links, python_version=self.python_version, @@ -99,9 +101,13 @@ def act(self) -> None: if "[FLAG-FileSerialize]" in self.package[:32]: self.package = self.package.strip("[FLAG-FileSerialize]") self.package = "-r " + FileSerialize.serialize2file(self.package) + if self.opts is None: + self.opts = "" + self.opts += "--target " + os.path.join(self.interpreter_path, + com_configs["RELATIVE_INTERPRETER_SITE_PACKAGES_PATH"]) pip.install_local_package(self.package, version=self.version, - whl_dir=os.path.join(self.module_path, "opt/packages"), + whl_dir=os.path.join(self.module_path, DOWN_PACKAGES_RELATIVE_PATH), no_dependent=self.no_dependent, opts=self.opts) @@ -144,6 +150,24 @@ def act(self) -> None: opts=self.opts) +class BatchInstallationOpt(SubModuleOpt): + def __init__(self, path=None): + super(BatchInstallationOpt, self).__init__(disposable=True) + self.path = path + + def act(self) -> None: + if self.path is None: + self.path = os.path.join(self.module_path, DOWN_PACKAGES_RELATIVE_PATH) + whl_list = [whl.split("-")[0] for whl in os.listdir(self.path)] + opts = "--target " + os.path.join(self.interpreter_path, + com_configs["RELATIVE_INTERPRETER_SITE_PACKAGES_PATH"]) + for whl_name in whl_list: + pip.install_local_package(whl_name, + whl_dir=self.path, + no_dependent=True, + opts=opts) + + class CustomPackage(SubModule): def __init__(self, package, @@ -192,7 +216,7 @@ def __init__(self, self.add_pack_opt(DownloadWhlOpt(package=requirements_file_path, no_dependent=False)) self.add_unpack_opt(LocalInstallWhlOpt(package=fs_data, - no_dependent=True)) + no_dependent=False)) elif deploy_mode == ONLINE_DEPLOY_MODE: self.add_unpack_opt(OnlineInstallWhlOpt(package=fs_data, no_dependent=False)) @@ -223,13 +247,17 @@ def __init__(self, Logging.info(f"正在分析{os.path.abspath(path)}下的依赖情况...") requirements = pip.analyze_dependence(path, return_path=False) + module_name_list = [m.name for m in module_list] # 对特殊包进行过滤和特殊化 for requirement in dict(requirements): if requirement in SPECIAL_MODULE: special_module, parameter = SPECIAL_MODULE[requirement] parameter["version"] = requirements[requirement] parameter["deploy_mode"] = deploy_mode - module_list.append(special_module(**parameter)) + module = special_module(**parameter) + # 如果开发者没有定义这个Module,那么则添加Module + if module.name not in module_name_list: + module_list.append(module) requirements.pop(requirement) # 保存依赖至 @@ -260,6 +288,14 @@ def __init__(self): no_dependent=False)) +class BatchInstallation(SubModule): + def __init__(self): + super().__init__() + self.level = LOW_LEVEL + if DEFAULT_DEPLOY_MODE == LOCAL_DOWNLOAD_DEPLOY_MODE: + self.add_unpack_opt(BatchInstallationOpt()) + + class PaddlePaddlePackage(CustomPackage): def __init__(self, version: str = None, @@ -271,7 +307,8 @@ def __init__(self, version=version, deploy_mode=deploy_mode) else: - raise Exception("暂不支持该模式,请等待后期更新") + # ToDo 增加Soft-CUDA + raise Exception("暂不支持PaddlePaddle模式,请等待近期更新") # Logging.warning("正在为PaddlePaddle添加CUDA支持...\n" # "请注意2.0版本的PaddlePaddle在添加CUDA支持后,即使用户没有合适的GPU设备," # "也将默认以GPU模式进行执行。若不添加判断/设备选择的代码,则可能会出现设备相关的报错!\n" diff --git a/setup.py b/setup.py index fe12ab0..045b49f 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ # python setup.py sdist bdist_wheel setup( name='QPT', - version='1.0a5', + version='1.0a6', packages=find_packages(), url='https://github.com/GT-ZhangAcer/QPT', license='LGPL',