├── down.ico ├── .gitignore ├── README.md ├── LICENSE ├── gui.py └── main.py /down.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengdeer589/Python_Download_Installation_Tool/HEAD/down.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /__pycache__ 3 | /.idea 4 | /releases 5 | /runtime 6 | main.exe 7 | main.cmd 8 | pyproject.toml 9 | .python-version 10 | /main.dist 11 | nuitka-crash-report.xml 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 程序说明 2 | python工具,使用tkinter实现gui设计,用于在线下载第三方库wheel文件,离线安装第三方库wheel文件。适合需要从外网导入第三方库进内网的朋友,本工具提供了在线下载和离线安装两个功能模块。 3 | # 在线下载 4 | 本模块基于pip download命令实现下载指定的第三方库,指定的requirement.txt所包含的库,实现将tar.gz文件构建为whl文件。使用前会自动读取系统python解释器位置,以此调用pip命令,可以自己指定python解释器,来实现指定python版本的第三方库下载。 5 | # 离线安装 6 | 本模块基于uv pip install命令实现安装指定的第三方库,指定的requirement.txt所包含的库,若指定的项目地址不存在虚拟环境,则使用uv v 命令创建。 7 | 8 | uv是一款python的第三方库,用于包管理及虚拟环境创建. 9 | 10 | tips:win7下,不支持uv 0.1.39以上版本。 11 | # 项目依赖 12 | 本项目基于python标准库实现,不包含第三方依赖。 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 mengdeer589 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gui.py: -------------------------------------------------------------------------------- 1 | """ 2 | 本代码由[Tkinter布局助手]生成 3 | 官网:https://www.pytk.net 4 | QQ交流群:905019785 5 | 在线反馈:https://support.qq.com/product/618914 6 | """ 7 | 8 | from tkinter import * # type: ignore[assignment] 9 | from tkinter.ttk import * # type: ignore[assignment] 10 | 11 | 12 | class WinGUI(Tk): 13 | def __init__(self): 14 | super().__init__() 15 | self.__win() 16 | self.tk_tabs_menu = self.__tk_tabs_menu(self) 17 | self.tk_label_frame_lw1wssni = self.__tk_label_frame_lw1wssni( 18 | self.tk_tabs_menu_0 19 | ) 20 | self.tk_label_lw1wt1v6 = self.__tk_label_lw1wt1v6(self.tk_label_frame_lw1wssni) 21 | self.tk_input_package = self.__tk_input_package(self.tk_label_frame_lw1wssni) 22 | self.tk_button_save_path = self.__tk_button_save_path( 23 | self.tk_label_frame_lw1wssni 24 | ) 25 | self.tk_input_path = self.__tk_input_path(self.tk_label_frame_lw1wssni) 26 | self.tk_select_box_download = self.__tk_select_box_download( 27 | self.tk_label_frame_lw1wssni 28 | ) 29 | self.tk_check_button_download = self.__tk_check_button_downdload( 30 | self.tk_label_frame_lw1wssni 31 | ) 32 | self.tk_label_frame_lw1wx3j1 = self.__tk_label_frame_lw1wx3j1( 33 | self.tk_tabs_menu_0 34 | ) 35 | self.tk_button_download_start = self.__tk_button_download_start( 36 | self.tk_label_frame_lw1wx3j1 37 | ) 38 | self.tk_button_open_path = self.__tk_button_open_path( 39 | self.tk_label_frame_lw1wx3j1 40 | ) 41 | self.tk_button_download_infor = self.__tk_button_download_infor( 42 | self.tk_label_frame_lw1wx3j1 43 | ) 44 | self.tk_label_frame_lw1xjzul = self.__tk_label_frame_lw1xjzul( 45 | self.tk_tabs_menu_1 46 | ) 47 | self.tk_label_file_store = self.__tk_label_file_store( 48 | self.tk_label_frame_lw1xjzul 49 | ) 50 | self.tk_label_target = self.__tk_label_target(self.tk_label_frame_lw1xjzul) 51 | self.tk_label_target_file = self.__tk_label_target_file( 52 | self.tk_label_frame_lw1xjzul 53 | ) 54 | self.tk_input_file_store = self.__tk_input_file_store( 55 | self.tk_label_frame_lw1xjzul 56 | ) 57 | self.tk_input_target = self.__tk_input_target(self.tk_label_frame_lw1xjzul) 58 | self.tk_input_target_file = self.__tk_input_target_file( 59 | self.tk_label_frame_lw1xjzul 60 | ) 61 | self.tk_label_frame_lw1xmj4y = self.__tk_label_frame_lw1xmj4y( 62 | self.tk_tabs_menu_1 63 | ) 64 | self.tk_button_install_start = self.__tk_button_install_start( 65 | self.tk_label_frame_lw1xmj4y 66 | ) 67 | self.tk_button_install_infor = self.__tk_button_install_infor( 68 | self.tk_label_frame_lw1xmj4y 69 | ) 70 | self.tk_button_install_uv = self.__tk_button_install_uv( 71 | self.tk_label_frame_lw1xmj4y 72 | ) 73 | self.tk_label_lw2524gb = self.__tk_label_lw2524gb(self) 74 | self.tk_input_python_path = self.__tk_input_python_path(self) 75 | self.tk_label_frame_log = self.__tk_label_frame_log(self) 76 | self.tk_text_log = self.__tk_text_log(self.tk_label_frame_log) 77 | 78 | def __win(self): 79 | self.title("Python第三方库下载安装工具") 80 | # 设置窗口大小、居中 81 | width = 695 82 | height = 640 83 | screenwidth = self.winfo_screenwidth() 84 | screenheight = self.winfo_screenheight() 85 | geometry = "%dx%d+%d+%d" % ( 86 | width, 87 | height, 88 | (screenwidth - width) / 2, 89 | (screenheight - height) / 2, 90 | ) 91 | self.geometry(geometry) 92 | 93 | self.resizable(width=False, height=False) 94 | 95 | def scrollbar_autohide(self, vbar, hbar, widget): 96 | """自动隐藏滚动条""" 97 | 98 | def show(): 99 | if vbar: 100 | vbar.lift(widget) 101 | if hbar: 102 | hbar.lift(widget) 103 | 104 | def hide(): 105 | if vbar: 106 | vbar.lower(widget) 107 | if hbar: 108 | hbar.lower(widget) 109 | 110 | hide() 111 | widget.bind("", lambda e: show()) 112 | if vbar: 113 | vbar.bind("", lambda e: show()) 114 | if vbar: 115 | vbar.bind("", lambda e: hide()) 116 | if hbar: 117 | hbar.bind("", lambda e: show()) 118 | if hbar: 119 | hbar.bind("", lambda e: hide()) 120 | widget.bind("", lambda e: hide()) 121 | 122 | def v_scrollbar(self, vbar, widget, x, y, w, h, pw, ph): 123 | widget.configure(yscrollcommand=vbar.set) 124 | vbar.config(command=widget.yview) 125 | vbar.place(relx=(w + x) / pw, rely=y / ph, relheight=h / ph, anchor="ne") 126 | 127 | def h_scrollbar(self, hbar, widget, x, y, w, h, pw, ph): 128 | widget.configure(xscrollcommand=hbar.set) 129 | hbar.config(command=widget.xview) 130 | hbar.place(relx=x / pw, rely=(y + h) / ph, relwidth=w / pw, anchor="sw") 131 | 132 | def create_bar(self, master, widget, is_vbar, is_hbar, x, y, w, h, pw, ph): 133 | vbar, hbar = None, None 134 | if is_vbar: 135 | vbar = Scrollbar(master) 136 | self.v_scrollbar(vbar, widget, x, y, w, h, pw, ph) 137 | if is_hbar: 138 | hbar = Scrollbar(master, orient="horizontal") 139 | self.h_scrollbar(hbar, widget, x, y, w, h, pw, ph) 140 | self.scrollbar_autohide(vbar, hbar, widget) 141 | 142 | def __tk_tabs_menu(self, parent): 143 | frame = Notebook(parent) 144 | self.tk_tabs_menu_0 = self.__tk_frame_menu_0(frame) 145 | frame.add(self.tk_tabs_menu_0, text="--在线-下载 --") 146 | self.tk_tabs_menu_1 = self.__tk_frame_menu_1(frame) 147 | frame.add(self.tk_tabs_menu_1, text="--离线-安装--") 148 | frame.place(x=0, y=0, width=685, height=290) 149 | return frame 150 | 151 | def __tk_frame_menu_0(self, parent): 152 | frame = Frame(parent) 153 | frame.place(x=0, y=0, width=685, height=290) 154 | return frame 155 | 156 | def __tk_frame_menu_1(self, parent): 157 | frame = Frame(parent) 158 | frame.place(x=0, y=0, width=685, height=290) 159 | return frame 160 | 161 | def __tk_label_frame_lw1wssni(self, parent): 162 | frame = LabelFrame( 163 | parent, 164 | text="输入", 165 | ) 166 | frame.place(x=0, y=0, width=670, height=110) 167 | return frame 168 | 169 | def __tk_label_lw1wt1v6(self, parent): 170 | label = Label( 171 | parent, 172 | text="第三方库名", 173 | anchor="center", 174 | ) 175 | label.place(x=0, y=0, width=80, height=30) 176 | return label 177 | 178 | def __tk_input_package(self, parent): 179 | ipt = Entry( 180 | parent, 181 | ) 182 | ipt.place(x=100, y=0, width=320, height=30) 183 | return ipt 184 | 185 | def __tk_button_save_path(self, parent): 186 | btn = Button( 187 | parent, 188 | text="保存路径", 189 | takefocus=False, 190 | ) 191 | btn.place(x=0, y=50, width=80, height=30) 192 | return btn 193 | 194 | def __tk_input_path(self, parent): 195 | ipt = Entry( 196 | parent, 197 | ) 198 | ipt.place(x=100, y=50, width=550, height=30) 199 | return ipt 200 | 201 | def __tk_select_box_download(self, parent): 202 | cb = Combobox( 203 | parent, 204 | state="readonly", 205 | ) 206 | cb["values"] = ("清华", "阿里", "豆瓣", "中科大", "百度") 207 | cb.place(x=590, y=0, width=60, height=30) 208 | return cb 209 | 210 | def __tk_check_button_downdload(self, parent): 211 | cb = Checkbutton( 212 | parent, 213 | text="指定镜像源", 214 | ) 215 | cb.place(x=450, y=0, width=120, height=30) 216 | return cb 217 | 218 | def __tk_label_frame_lw1wx3j1(self, parent): 219 | frame = LabelFrame( 220 | parent, 221 | text="操作", 222 | ) 223 | frame.place(x=0, y=110, width=670, height=100) 224 | return frame 225 | 226 | def __tk_button_download_start(self, parent): 227 | btn = Button( 228 | parent, 229 | text="开始下载", 230 | takefocus=False, 231 | ) 232 | btn.place(x=100, y=20, width=80, height=30) 233 | return btn 234 | 235 | def __tk_button_open_path(self, parent): 236 | btn = Button( 237 | parent, 238 | text="打开路径", 239 | takefocus=False, 240 | ) 241 | btn.place(x=300, y=20, width=80, height=30) 242 | return btn 243 | 244 | def __tk_button_download_infor(self, parent): 245 | btn = Button( 246 | parent, 247 | text="使用说明", 248 | takefocus=False, 249 | ) 250 | btn.place(x=500, y=20, width=80, height=30) 251 | return btn 252 | 253 | def __tk_label_frame_lw1xjzul(self, parent): 254 | frame = LabelFrame( 255 | parent, 256 | text="输入", 257 | ) 258 | frame.place(x=0, y=0, width=670, height=150) 259 | return frame 260 | 261 | def __tk_label_file_store(self, parent): 262 | label = Label( 263 | parent, 264 | text="库文件夹", 265 | anchor="center", 266 | ) 267 | label.place(x=0, y=5, width=80, height=30) 268 | return label 269 | 270 | def __tk_label_target(self, parent): 271 | label = Label( 272 | parent, 273 | text="目标环境", 274 | anchor="center", 275 | ) 276 | label.place(x=0, y=50, width=80, height=30) 277 | return label 278 | 279 | def __tk_label_target_file(self, parent): 280 | label = Label( 281 | parent, 282 | text="库名", 283 | anchor="center", 284 | ) 285 | label.place(x=0, y=95, width=80, height=30) 286 | return label 287 | 288 | def __tk_input_file_store(self, parent): 289 | ipt = Entry( 290 | parent, 291 | ) 292 | ipt.place(x=100, y=5, width=550, height=30) 293 | return ipt 294 | 295 | def __tk_input_target(self, parent): 296 | ipt = Entry( 297 | parent, 298 | ) 299 | ipt.place(x=100, y=50, width=550, height=30) 300 | return ipt 301 | 302 | def __tk_input_target_file(self, parent): 303 | ipt = Entry( 304 | parent, 305 | ) 306 | ipt.place(x=100, y=95, width=550, height=30) 307 | return ipt 308 | 309 | def __tk_label_frame_lw1xmj4y(self, parent): 310 | frame = LabelFrame( 311 | parent, 312 | text="操作", 313 | ) 314 | frame.place(x=0, y=155, width=670, height=60) 315 | return frame 316 | 317 | def __tk_button_install_start(self, parent): 318 | btn = Button( 319 | parent, 320 | text="开始安装", 321 | takefocus=False, 322 | ) 323 | btn.place(x=100, y=0, width=80, height=30) 324 | return btn 325 | 326 | def __tk_button_install_infor(self, parent): 327 | btn = Button( 328 | parent, 329 | text="使用说明", 330 | takefocus=False, 331 | ) 332 | btn.place(x=480, y=0, width=80, height=30) 333 | return btn 334 | 335 | def __tk_button_install_uv(self, parent): 336 | btn = Button( 337 | parent, 338 | text="安装UV工具", 339 | takefocus=False, 340 | ) 341 | btn.place(x=266, y=0, width=80, height=30) 342 | return btn 343 | 344 | def __tk_label_lw2524gb(self, parent): 345 | label = Label( 346 | parent, 347 | text="Python解释器", 348 | anchor="center", 349 | ) 350 | label.place(x=0, y=250, width=80, height=30) 351 | return label 352 | 353 | def __tk_input_python_path(self, parent): 354 | ipt = Entry( 355 | parent, 356 | ) 357 | ipt.place(x=100, y=250, width=550, height=30) 358 | return ipt 359 | 360 | def __tk_label_frame_log(self, parent): 361 | frame = LabelFrame( 362 | parent, 363 | text="工作日志", 364 | ) 365 | frame.place(x=0, y=300, width=685, height=330) 366 | return frame 367 | 368 | def __tk_text_log(self, parent): 369 | text = Text(parent) 370 | text.place(x=0, y=0, width=675, height=300) 371 | return text 372 | 373 | 374 | class Win(WinGUI): 375 | def __init__(self, controller): 376 | self.ctl = controller 377 | super().__init__() 378 | self.__event_bind() 379 | self.__style_config() 380 | self.ctl.init(self) 381 | 382 | def __event_bind(self): 383 | self.tk_tabs_menu.bind("", self.ctl.哈哈哈) 384 | pass 385 | 386 | def __style_config(self): 387 | pass 388 | 389 | 390 | if __name__ == "__main__": 391 | win = WinGUI() 392 | win.mainloop() 393 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """工具main模块定义""" 2 | 3 | import os 4 | import platform 5 | import re 6 | import shutil 7 | import subprocess 8 | import sys 9 | import tarfile 10 | import threading 11 | from pathlib import Path 12 | from queue import Queue 13 | from tkinter import ACTIVE, DISABLED, BooleanVar, filedialog, messagebox 14 | from gui import WinGUI 15 | DEFAULT_PKG_DIR=r"E:\导入\第三方库导入" 16 | print(platform.release()) 17 | if sys.platform.startswith('win'): 18 | DEFAULT_COMMAND=["where","python"] 19 | UV_ACTIVATE_SCRIPT = ".venv/Scripts/activate.bat" 20 | UV_DEACTIVATE_SCRIPT = ".venv/Scripts/deactivate.bat" 21 | elif sys.platform.startswith('linux'): 22 | DEFAULT_COMMAND=["which","python"] 23 | UV_ACTIVATE_SCRIPT = ".venv/bin/activate" 24 | UV_DEACTIVATE_SCRIPT = "deactivate" 25 | else: 26 | print("不支持当前系统") 27 | os.system("pause") 28 | sys.exit() 29 | 30 | class ReStd(Queue): 31 | """继承Queue,实现重定向数据传输""" 32 | 33 | def __init__(self): 34 | Queue.__init__(self) 35 | 36 | def write(self, content): 37 | """将消息放入队列""" 38 | self.put(content) 39 | 40 | def flush(self): 41 | """定义flush""" 42 | return 43 | 44 | 45 | def execute_command(command: str, cwd: Path|None = None,env_updates:dict|None=None) -> tuple: 46 | """使用subprocess执行下载命令,并捕获输出""" 47 | env = os.environ.copy() 48 | if env_updates: 49 | env.update(env_updates) 50 | try: 51 | output_lines = [] 52 | with subprocess.Popen( 53 | command, 54 | cwd=cwd, 55 | stdout=subprocess.PIPE, 56 | stderr=subprocess.STDOUT, 57 | text=True, 58 | shell=True, 59 | # encoding="utf-8", 60 | env=env, 61 | universal_newlines=True, 62 | ) as process: 63 | if process.stdout is not None: 64 | for line in iter(process.stdout.readline, ''): 65 | print(line, end="", flush=True) 66 | output_lines.append(line) 67 | captured_output = "".join(output_lines) 68 | else: 69 | captured_output = "" 70 | process.wait() 71 | 72 | if process.returncode != 0: 73 | raise subprocess.CalledProcessError(process.returncode, command) 74 | return process, captured_output, process.returncode 75 | 76 | except FileNotFoundError: 77 | print(f"无法找到命令:{command.split()[0]}") 78 | return None, "命令未找到", -1 79 | 80 | except Exception as e: 81 | print(f"执行命令时发生错误:{str(e)},可能不存在对应安装包") 82 | return None, f"执行错误:{str(e)}", -2 83 | 84 | 85 | class DownLoad(WinGUI): 86 | """用于下载页功能实现类""" 87 | 88 | def __init__(self): 89 | super().__init__() 90 | self.v2 = BooleanVar(value=True) 91 | self.python_path = None 92 | self.download_path = "" 93 | self.target_package = None 94 | self.init_components() 95 | self.download_link = { 96 | "清华": "https://pypi.tuna.tsinghua.edu.cn/simple/", 97 | "阿里": "https://mirrors.aliyun.com/pypi/simple/", 98 | "豆瓣": "https://pypi.douban.com/simple/", 99 | "中科大": "https://pypi.mirrors.ustc.edu.cn/simple/", 100 | "百度": "https://mirror.baidu.com/pypi/simple", 101 | } 102 | 103 | def init_components(self): 104 | """初始化界面""" 105 | self.configure_gui() 106 | self.bind_events() 107 | 108 | def configure_gui(self): 109 | """初始化界面参数""" 110 | self.tk_check_button_download.config(variable=self.v2) 111 | self.tk_input_path.config(state="readonly") 112 | self.tk_select_box_download.current(0) 113 | if "runtime" in sys.exec_prefix: # 区别打包环境 114 | icon_file = Path.cwd().joinpath("runtime/down.ico") 115 | else: 116 | icon_file = Path.cwd().joinpath("down.ico") 117 | if sys.platform.startswith('win'): 118 | self.wm_iconbitmap(str(icon_file)) 119 | self.get_python_path() 120 | 121 | def get_python_path(self): 122 | """获取python路径""" 123 | result = None 124 | if sys.platform.startswith('win'): 125 | try: 126 | result = subprocess.run( 127 | ["where","python"], 128 | capture_output=True, 129 | text=True, 130 | check=True, 131 | ) 132 | except subprocess.CalledProcessError as e: 133 | print("找不到python,请设置!") 134 | return 135 | if result.returncode != 0: 136 | print("找不到python,请设置!") 137 | elif sys.platform.startswith('linux'): 138 | for python in {"python","python3"}: 139 | try: 140 | result = subprocess.run( 141 | ["which",python], 142 | capture_output=True, 143 | text=True, 144 | check=True, 145 | ) 146 | except subprocess.CalledProcessError as e: 147 | continue 148 | if result.returncode != 0: 149 | continue 150 | break 151 | python_path = str(result.stdout).split("\n", maxsplit=1)[0] 152 | if Path(python_path).exists() and "WindowsApps" not in python_path: 153 | self.tk_input_python_path.delete(0, "end") 154 | self.tk_input_python_path.insert(0, python_path) 155 | 156 | def bind_events(self): 157 | """绑定点击事件""" 158 | self.tk_button_download_start.config(command=self.start_download) 159 | self.tk_button_save_path.config(command=self.save_path) 160 | self.tk_button_open_path.config(command=self.open_path) 161 | self.tk_button_download_infor.config(command=self.show_system_info) 162 | 163 | @staticmethod 164 | def show_system_info() -> None: 165 | """下载页使用说明""" 166 | message = ( 167 | "本页用于下载第三方库文件,包括指定库及其依赖,自动构建wheel文件" 168 | "并将下载的文件存放于指定目录。\n要求系统已安装Python环境。\n\n" 169 | "实现原理:利用pip download功能下载第三方库。\n\n" 170 | "首先给出python解释器的路径,用它来调用pip工具,以此确定第三方库python版本\n" 171 | "使用示例:\n" 172 | "第三方库名:pandas\n" 173 | "第三方库名:-r path-to-requirements.txt\n" 174 | "第三方库名:-r path-to-requirements.txt 跟一切' pip download '中的可用命令\n" 175 | ) 176 | messagebox.showinfo(title="程序说明", message=message) 177 | 178 | def save_path(self) -> None: 179 | """设置库文件保存位置""" 180 | self.download_path = filedialog.askdirectory() 181 | self.tk_input_path.config(state="normal") 182 | self.tk_input_path.delete(0, "end") 183 | self.tk_input_path.insert(0, self.download_path or "") 184 | self.tk_input_path.config(state="readonly") 185 | 186 | def open_path(self) -> None: 187 | """打开文件夹""" 188 | if self.download_path: 189 | if sys.platform.startswith('win'): 190 | os.startfile(self.download_path) 191 | 192 | 193 | def start_download(self) -> None: 194 | """开始下载""" 195 | self.python_path = self.tk_input_python_path.get() 196 | if not self.python_path: 197 | messagebox.showinfo(title="提示", message="请设置python解释器路径") 198 | return 199 | self.target_package = self.tk_input_package.get().strip() 200 | if not self.download_path or not self.target_package: 201 | messagebox.showerror("错误", "请输入正确的第三方库名与保存路径!") 202 | return 203 | self.tk_text_log.delete(1.0, "end") 204 | down_thread = threading.Thread(target=self.download_thread_func, daemon=True) 205 | down_thread.start() 206 | # with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: 207 | # future = executor.submit(self.download_thread_func) 208 | self.tk_button_download_start.config(state=DISABLED) 209 | 210 | def download_thread_func(self) -> None: 211 | """下载线程,函数实现""" 212 | command = self.construct_pip_command() 213 | _, stdout, return_code = execute_command(command) 214 | if return_code == 0: 215 | print("完成下载".center(80, "*")) 216 | self.extract_downloaded_files(stdout) 217 | folder_path = Path(self.download_path).joinpath("out") 218 | if folder_path.exists(): 219 | try: 220 | shutil.rmtree(folder_path) 221 | print(f"文件夹 '{folder_path}' 及其内容已被成功删除。") 222 | except OSError as e: 223 | print(f"删除文件夹时发生错误: {e.strerror}") 224 | messagebox.showinfo(title="成功", message="已完成所有下载!") 225 | 226 | else: 227 | print("下载失败\n") 228 | self.tk_button_download_start.config(state=ACTIVE) 229 | 230 | def construct_pip_command(self) -> str: 231 | """构建下载命令""" 232 | pip_command = [ 233 | f'"{self.python_path}"', 234 | "-m", 235 | "pip", 236 | "download", 237 | "-d", 238 | f'"{self.download_path}"', 239 | self.target_package, 240 | ] 241 | if self.v2.get(): 242 | mirror = self.download_link[self.tk_select_box_download.get()] 243 | pip_command.extend(["-i", mirror]) 244 | return " ".join(pip_command) 245 | 246 | def extract_downloaded_files(self, output: str) -> None: 247 | """释放下载的文件""" 248 | pattern = r"(?<=Saved)(.*)(?=Successfully)" # 正则表达式模式 249 | matches = re.findall( 250 | pattern, output, re.DOTALL 251 | ) # 使用re.DOTALL使.匹配包括换行符在内的任意字符 252 | extracted_text = matches[0].strip() if matches else None 253 | file_names = [] 254 | if extracted_text: 255 | file_names = extracted_text.replace("Saved ", "").split("\n") 256 | file_names = [ 257 | Path(self.download_path).joinpath(Path(file).name) 258 | for file in file_names 259 | ] 260 | if "already downloaded" in output: 261 | pattern = r"File was already downloaded\s+(.*?\.(?:tar\.gz|whl))" 262 | 263 | # 使用findall方法找到所有匹配项 264 | file_paths = re.findall(pattern, output) 265 | 266 | file_names.extend(file_paths) 267 | file_names = [ 268 | Path(self.download_path).joinpath(Path(file).name) for file in file_names 269 | ] 270 | self.make_wheels(file_names) 271 | 272 | def make_wheels(self, whl_names: list[Path]) -> None: 273 | """将.tar.gz文件编译为wheel""" 274 | tars = self.check_tar_gz(whl_names) 275 | if tars: 276 | print(f"发现tar.gz压缩包,合计{len(tars)}个,开始自动构建wheel文件") 277 | total_count = len(tars) 278 | for idx, tar in enumerate(tars): 279 | self.build_wheel(tar, self.download_path) 280 | print(f"剩余{total_count - 1 - idx}个还未构建".center(80, "*")) 281 | 282 | @staticmethod 283 | def check_tar_gz(file_list: list[Path]) -> list[Path]: 284 | """检查是否存在.tar.gz文件,该文件需在线编译""" 285 | tars = [] 286 | for file_name in file_list: 287 | if file_name.suffix == ".gz": 288 | tars.append(file_name) 289 | return tars 290 | 291 | def build_wheel(self, tar: Path, download_path: str) -> None: 292 | """使用pip wheel . 命令构建wheel文件""" 293 | with tarfile.open(tar, "r:gz") as tar_sub: 294 | tar_sub.extractall(path=Path(download_path).joinpath("out")) 295 | command = [f"{self.python_path}", "-m", "pip", "wheel", "."] 296 | 297 | dir_name = str(tar.stem).replace(".tar", "") 298 | current_path = str(Path().cwd()) 299 | cwd = Path(download_path).joinpath(f"out/{dir_name}") 300 | command_str = " ".join(command) 301 | env={"PIP_INDEX_URL":"https://mirrors.aliyun.com/pypi/simple/","PIP_TRUSTED_HOST":"mirrors.aliyun.com"} 302 | result, stdout, return_code = execute_command(command_str, cwd=cwd,env_updates=env) 303 | if return_code != 0: 304 | print(f"wheel文件构建失败:\n{result.stderr}") 305 | os.chdir(current_path) 306 | return 307 | print(f"{tar.stem}:wheel文件构建成功".center(80, "*")) 308 | 309 | self.get_wheel_name( 310 | stdout, 311 | download_path, 312 | Path(download_path).joinpath(f"out/{dir_name}"), 313 | ) 314 | os.chdir(current_path) 315 | 316 | def get_wheel_name(self, message: str, download_path: str, file_path: Path) -> None: 317 | """获取编译生成的wheel文件名""" 318 | pattern = r"filename=(.+\.whl)" 319 | match:re.Match|None = re.search(pattern, message) 320 | if match is None: 321 | return 322 | try: 323 | filename = match.group(1) 324 | file_path = file_path.joinpath(filename) 325 | self.move_wheel_file(file_path, Path(download_path).joinpath(filename)) 326 | except AttributeError: 327 | pass 328 | 329 | @staticmethod 330 | def move_wheel_file(filename: Path, new_path: Path) -> None: 331 | """将生成的wheel文件移动到设置的库下载文件夹""" 332 | try: 333 | filename.rename(new_path) 334 | except FileExistsError: 335 | pass 336 | 337 | 338 | class Install(WinGUI): 339 | """库安装类定义""" 340 | 341 | def __init__(self): 342 | super().__init__() 343 | self.uv = None 344 | self.ini_window() 345 | 346 | def ini_window(self): 347 | """初始化界面""" 348 | self.tk_button_install_start.config(command=self.start) 349 | self.tk_button_install_infor.config(command=self.install_infor) 350 | self.tk_button_install_uv.config(command=self.install_uv) 351 | self.tk_input_file_store.delete(0, "end") 352 | self.tk_input_file_store.insert(0, DEFAULT_PKG_DIR) 353 | self.tk_input_target.delete(0, "end") 354 | # self.tk_input_target.insert(0, r"D:\python\fastui") 355 | self.tk_input_target_file.delete(0, "end") 356 | self.tk_input_target_file.insert(0, "matplotlib") 357 | self.uv = None 358 | 359 | @staticmethod 360 | def check_uv() -> bool: 361 | """用于检测是否安装了UV""" 362 | command_str = "uv -V" 363 | _, _, return_code = execute_command(command_str) 364 | if return_code != 0: 365 | messagebox.showwarning( 366 | title="警告", message="UV工具不存在,无法使用离线安装功能" 367 | ) 368 | return False 369 | return True 370 | 371 | def install_uv(self): 372 | python_path = self.tk_input_python_path.get() 373 | if not python_path: 374 | messagebox.showerror( 375 | title="指定python路径", 376 | message="请将python解释器绝对路径复制粘贴到python解释器输入框中", 377 | ) 378 | return 379 | uv_file = Path.cwd().joinpath( 380 | "runtime/uv-0.1.39-py3-none-win_amd64.whl" 381 | ) # 该版本支持win7 382 | command = [ 383 | f'"{python_path}"', 384 | "-m", 385 | "pip", 386 | "install", 387 | f'"{uv_file}"', 388 | "--no-cache-dir", 389 | "--no-index", 390 | ] 391 | command_str = " ".join(command) 392 | print(command_str) 393 | _, _, return_code = execute_command(command_str) 394 | if return_code != 0: 395 | print("uv工具安装失败\n") 396 | self.tk_button_install_start.config(state=ACTIVE) 397 | return 398 | print("uv工具安装成功") 399 | 400 | def start(self) -> None: 401 | """开始安装第三方库""" 402 | if self.uv: 403 | install_thread = threading.Thread(target=self.thread_func, daemon=True) 404 | self.tk_button_install_start.config(state=DISABLED) 405 | install_thread.start() 406 | else: 407 | print( 408 | "虚拟环境创建失败,请使用' pip install uv '或" 409 | "' pip install uv --no-index -f path-to-uv-wheel ' 安装UV工具\n" 410 | "或点击 安装UV工具 按钮进行安装" 411 | ) 412 | return 413 | 414 | # pip install uv --no-cache-dir --no-index -f E:\导入\测试 415 | def thread_func(self) -> None: 416 | """线程,安装第三方库""" 417 | target_path = self.tk_input_target.get() 418 | target_file = self.tk_input_target_file.get() 419 | if "-r" in target_file: 420 | target_file = target_file.split(" ") 421 | else: 422 | target_file = [target_file] 423 | file_store = self.tk_input_file_store.get() 424 | # 判断项目下是否存在venv环境 425 | 426 | venv_path = Path(target_path).joinpath(UV_ACTIVATE_SCRIPT) 427 | cwd = Path(target_path) 428 | if not venv_path.exists(): 429 | command = ["uv", "venv", f'--python="{self.tk_input_python_path.get()}"'] 430 | command_str = " ".join(command) 431 | 432 | _, _, return_code = execute_command(command_str, cwd=cwd) 433 | if return_code != 0: 434 | print("虚拟环境创建失败,请使用' pip install uv ' 安装UV工具\n") 435 | self.tk_button_install_start.config(state=ACTIVE) 436 | return 437 | print("虚拟环境创建成功") 438 | self.tk_button_install_start.config(state=ACTIVE) 439 | activate_cmd=venv_path if sys.platform.startswith("win") else f"source {UV_ACTIVATE_SCRIPT}" 440 | deactivate_cmd = venv_path.parent.joinpath("deactivate.bat") if sys.platform.startswith("win") else "deactivate" 441 | commands = [ 442 | [str(activate_cmd)], 443 | ["uv", "pip", "install", "--offline", f'-f="{file_store}"', *target_file], 444 | [str(deactivate_cmd)], 445 | ] 446 | for command in commands: 447 | command_str = " ".join(command) 448 | _, _, return_code = execute_command(command_str, cwd=cwd) 449 | if return_code != 0: 450 | print(f"第三方库安装失败:\n{return_code}") 451 | self.tk_button_install_start.config(state=ACTIVE) 452 | return 453 | print("第三方库安装成功!".center(80, "*")) 454 | messagebox.showinfo(title="安装完成", message="第三方库安装成功!") 455 | 456 | @staticmethod 457 | def install_infor() -> None: 458 | """安装页使用说明""" 459 | message = ( 460 | "实现原理:利用' uv pip install --offline -f=path-to-wheel '来实现离线快速安装。" 461 | "若指定的文件夹虚拟环境不存在,会自动使用' uv v '命令创建虚拟环境\n\n" 462 | "库文件夹:第三方库wheel文件所在文件夹;\n" 463 | "目标环境:第三方库安装位置,若该处不存在虚拟环境,将使用'uv v'命令创建虚拟环境;\n" 464 | "库名:需要安装的第三方库名字,如matplotlib。\n" 465 | "使用示例\n" 466 | "库名:matplotlib\n" 467 | "库名:-r path-to-requirements.txt\n" 468 | "库名:-r path-to-requirements.txt 跟一切'uv pip install '中的可用命令\n" 469 | ) 470 | messagebox.showinfo(title="使用说明", message=message) 471 | 472 | 473 | class MainWindow(DownLoad, Install): 474 | """继承两个功能页""" 475 | 476 | def __init__(self): 477 | super().__init__() 478 | self.queue = ReStd() # 重定向输出 479 | self.tk_tabs_menu.bind("<>", self.change_menu) 480 | self.__init_gui() 481 | 482 | def __init_gui(self) -> None: 483 | """重定向输出,并进入show_msg循环""" 484 | sys.stdout = self.queue 485 | self.after(100, self.show_msg) 486 | 487 | def show_msg(self): 488 | """使用after方法更新主界面""" 489 | while not self.queue.empty(): 490 | content = self.queue.get() 491 | self.tk_text_log.insert("end", content) 492 | self.tk_text_log.see("end") 493 | self.after(100, self.show_msg) 494 | 495 | def change_menu(self, _) -> None: 496 | """设置切换事件,若第三方库,库目录已设置,则自动复制到安装页""" 497 | current_tab_idx=self.tk_tabs_menu.index(self.tk_tabs_menu.select()) 498 | if self.uv is None and current_tab_idx: 499 | self.uv=self.check_uv() 500 | download_path = self.tk_input_path.get() 501 | target_package = self.tk_input_package.get() 502 | if current_tab_idx and download_path: 503 | self.tk_input_file_store.delete(0, "end") 504 | self.tk_input_file_store.insert(0, download_path) 505 | if current_tab_idx and target_package: 506 | self.tk_input_target_file.delete(0, "end") 507 | self.tk_input_target_file.insert(0, target_package) 508 | 509 | 510 | if __name__ == "__main__": 511 | 512 | 513 | window = MainWindow() 514 | window.mainloop() 515 | 516 | #nuitka --windows-icon-from-ico=down.ico main.py 517 | --------------------------------------------------------------------------------