├── .gitignore ├── __init__.py ├── README.md ├── pyproject.toml ├── LICENSE └── psgnuitka.py /.gitignore: -------------------------------------------------------------------------------- 1 | poetry.lock 2 | dist/ -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from . import psgnuitka 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # psgnuitka - a GUI tool to build app with nuitka 2 | 3 | --- 4 | 5 | 本工具还处于早期开发阶段,部分功能和想法还在技术实现和优化中: 6 | 7 | 目标想要实现的功能有: 8 | 9 | - 自动检索可用的Python解释器位置 10 | - 自动创建venv环境用于干净环境的打包构建 11 | - 可内置相关的Nuitka所需的工具链软件方便统一构建环境 12 | - 提供对多版本Nuitka的支持 13 | - 支持多国语言 14 | - 按照使用者的级别提供不同级别的构建界面 15 | - 支持Windows、Linux、Mac OS 16 | 17 | --- 18 | 19 | 20 | 21 | ## 概述 22 | 23 | Nuitka是一个Python的编译器,可以无缝替换或者扩展Python解释器 24 | 25 | Nuitka会将Python模块转换为C语言程序,从而利用C语言的编译工具链构建可执行文件 26 | 27 | Nuitka通常能够让你的Python程序获得更高的运行性能,不过因为需要利用传统的C语言编译,所以在第一次执行之前需要较长时间的构 28 | 29 | Nuitka需要使用命令行的方式进行调用,通常构建一般的Python脚本需要较长的命令 30 | 31 | psgnuitka工具就是用来解决Nuitka命令太长且复杂的一个工具,通过图形化界面引导,只需要轻松几步,就能快速生成需要的命令,并可以支持一键调用 32 | 33 | 你可以使用pip命令很方便的安装此工具,或者可以通过 `github`仓库获取二进制可发行版本 34 | 35 | 目前此工具还处于早期开发阶段,存在部分设计不合理和功能缺失 36 | 37 | 如有问题或者建议,可以通过issue或者邮件交流 38 | 39 | --- 40 | 41 | 本工具的开发离不开如下工具的帮助 42 | 43 | Nuitka 44 | 45 | PySimpleGUI 46 | 47 | Python 48 | 49 | Poetry 50 | 51 | --- 52 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "psgnuitka" 3 | version = "0.2.8" 4 | description = "A graphical tool to facilitate the generation of Python executables using Nuitka" 5 | authors = ["boulderh "] 6 | readme = "README.md" 7 | license = "MIT" 8 | homepage = "https://github.com/BoulderH/psgnuitka" 9 | repository = "https://github.com/BoulderH/psgnuitka" 10 | keywords = ["nuitka","PySimpleGUI","psgnuitka"] 11 | include = [ 12 | "LICENSE", 13 | "README.md" 14 | ] 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.8" 18 | pysimplegui = "^4.60.0" 19 | 20 | 21 | [[tool.poetry.source]] 22 | name = "mirrors" 23 | url = "https://pypi.tuna.tsinghua.edu.cn/simple/" 24 | default = true 25 | secondary = false 26 | 27 | 28 | [tool.poetry.group.dev.dependencies] 29 | autopep8 = "^2.0.2" 30 | nuitka = "1.4.*" 31 | zstandard = "^0.20.0" 32 | 33 | [build-system] 34 | requires = ["poetry-core"] 35 | build-backend = "poetry.core.masonry.api" 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 BoulderH 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 | -------------------------------------------------------------------------------- /psgnuitka.py: -------------------------------------------------------------------------------- 1 | import PySimpleGUI 2 | import pathlib 3 | import subprocess 4 | import time 5 | import locale 6 | import os 7 | 8 | 9 | def collectinfomation() -> dict: 10 | info = dict() 11 | sys_language = locale.getlocale()[0] 12 | language_code = locale.getpreferredencoding(do_setlocale=False) 13 | if sys_language == "Chinese (Simplified)_China": 14 | sys_language = "zh_CN" 15 | info['sys_language'] = sys_language 16 | info['language_code'] = language_code 17 | return info 18 | 19 | 20 | def runbuildcommand(command: str, window: PySimpleGUI.Window = None, timeout: int = None, system_encode: str = None) -> tuple: 21 | p = subprocess.Popen(command, shell=True, 22 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 23 | # if PySimpleGUI.running_windows(): 24 | # # command = shlex.split(command) 25 | # p = subprocess.Popen( 26 | # command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 27 | # else: 28 | # command = shlex.split(command) 29 | # p = subprocess.Popen( 30 | # command, shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 31 | output = '' 32 | for line in p.stdout: 33 | # line = line.decode('cp936', errors='replace' if (sys.version_info) < (3, 5) else 'backslashreplace').rstrip() 34 | line = line.decode(system_encode) 35 | output += line 36 | print(line, end="", flush=True) 37 | window.Refresh() if window else None # yes, a 1-line if, so shoot me 38 | retval = p.wait(timeout) 39 | return retval, output # also return the output just for fun 40 | 41 | 42 | if __name__ == '__main__': 43 | sys_info = collectinfomation() 44 | PySimpleGUI.theme("DarkGrey6") 45 | all_text = { 46 | "en_US": { 47 | "PYTHON-PATH": "Select Python Interpreter", 48 | "FileBrowse-Button": "Browse File", 49 | "FolderBrowse-Button": "Browse Dir", 50 | "PY-PATH": "Select Python Script" 51 | }, 52 | "zh_CN": { 53 | "PYTHON-PATH": "使用的Python解释器位置", 54 | "FileBrowse-Button": "选择文件", 55 | "FolderBrowse-Button": "选择目录", 56 | "PY-PATH": "选择Python脚本文件", 57 | } 58 | } 59 | sys_info 60 | text = all_text.get(sys_info['sys_language'], "zh_CN") 61 | # text=all_text[sys_info['sys_language']] 62 | 63 | # 主体布局 64 | main_layout = [ 65 | [ 66 | PySimpleGUI.Text("使用的Python解释器位置"), 67 | PySimpleGUI.Input(key="PYTHON-PATH"), 68 | PySimpleGUI.FileBrowse(button_text="选择文件", file_types=[ 69 | ("", "python.exe python")], target="PYTHON-PATH"), 70 | PySimpleGUI.Text("选择Python脚本文件"), 71 | PySimpleGUI.Input(key="PY-PATH"), 72 | PySimpleGUI.FileBrowse(button_text="选择文件", file_types=[("", "*.py")], target="PY-PATH")], 73 | [ 74 | ], 75 | [ 76 | PySimpleGUI.Text("构建生成目录"), 77 | PySimpleGUI.Input(key="OUTPUT-DIR"), 78 | PySimpleGUI.FolderBrowse(button_text="选择目录", target="OUTPUT-DIR"), 79 | PySimpleGUI.Text("可执行文件名"), 80 | PySimpleGUI.Input(key="OUTPUT-FILENAME"), 81 | ], 82 | [ 83 | PySimpleGUI.Frame("C编译器选择", layout=[ 84 | [ 85 | PySimpleGUI.Radio(group_id="CCOMPILER", 86 | text="MingW64", default=True, key="MINGW64"), 87 | PySimpleGUI.Radio(group_id="CCOMPILER", 88 | text="Clang", default=False, key="CLANG"), 89 | PySimpleGUI.Radio(group_id="CCOMPILER", 90 | text="MSVC", default=False, key="MSVC"), 91 | ] 92 | ]), 93 | PySimpleGUI.Frame("单文件模式", layout=[[ 94 | PySimpleGUI.Checkbox( 95 | text="onefile", default=False, key="ONEFILE") 96 | ]]), 97 | PySimpleGUI.Frame("是否控制台可用", layout=[ 98 | [ 99 | PySimpleGUI.Radio( 100 | group_id="CONSOLE", text="enable", default=True, key="ENABLE-CONSOLE"), 101 | PySimpleGUI.Radio( 102 | group_id="CONSOLE", text="disable", default=False, key="DISABLE-CONSOLE"), 103 | ] 104 | ]), 105 | PySimpleGUI.Frame("工具链来源选择", layout=[ 106 | [ 107 | PySimpleGUI.Radio( 108 | group_id="TOOLCHAINFROM", text="使用系统环境路径", default=False, key="SYSTEMTOOLCHAIN"), 109 | PySimpleGUI.Radio(group_id="TOOLCHAINFROM", text="使用本工具自带版本", 110 | default=False, key="LOCALTOOLCHAIN", disabled=True), 111 | PySimpleGUI.Radio( 112 | group_id="TOOLCHAINFROM", text="由nuitka自动下载处理", default=True, key="NUITKATOOLCHAIN"), 113 | ] 114 | ]), 115 | ], 116 | [ 117 | PySimpleGUI.Frame("额外插件", layout=[ 118 | [ 119 | PySimpleGUI.Checkbox( 120 | text="eventlet", default=False, key="EVENTLET"), 121 | PySimpleGUI.Checkbox( 122 | text="gevent", default=False, key="GEVENT"), 123 | PySimpleGUI.Checkbox(text="gi", default=False, key="GI"), 124 | PySimpleGUI.Checkbox( 125 | text="glfw", default=False, key="GLFW"), 126 | PySimpleGUI.Checkbox( 127 | text="kivy ", default=False, key="KIVY"), 128 | 129 | ], 130 | [ 131 | PySimpleGUI.Checkbox( 132 | text="matplotlib", default=False, key="MATPLOTLIB"), 133 | PySimpleGUI.Checkbox( 134 | text="no-qt", default=False, key="NO-QT"), 135 | PySimpleGUI.Checkbox( 136 | text="numpy", default=False, key="NUMPY"), 137 | PySimpleGUI.Checkbox( 138 | text="pmw-freezer", default=False, key="PMW-FREEZER"), 139 | PySimpleGUI.Checkbox( 140 | text="pylint-warnings", default=False, key="PYLINT-WARNINGS"), 141 | 142 | ], 143 | [ 144 | 145 | PySimpleGUI.Checkbox( 146 | text="pyqt5", default=False, key="PYQT5"), 147 | PySimpleGUI.Checkbox( 148 | text="pyqt6", default=False, key="PYQT6"), 149 | PySimpleGUI.Checkbox( 150 | text="pyside2", default=False, key="PYSIDE2"), 151 | PySimpleGUI.Checkbox( 152 | text="pyside6", default=False, key="PYSIDE6"), 153 | PySimpleGUI.Checkbox( 154 | text="pywebview", default=False, key="PYWEBVIEW"), 155 | 156 | ], 157 | [ 158 | 159 | PySimpleGUI.Checkbox( 160 | text="tensorflow", default=False, key="TENSORFLOW"), 161 | PySimpleGUI.Checkbox( 162 | text="tk-inter", default=False, key="TK-INTER"), 163 | PySimpleGUI.Checkbox( 164 | text="torch", default=False, key="TORCH"), 165 | PySimpleGUI.Checkbox( 166 | text="trio", default=False, key="TRIO"), 167 | PySimpleGUI.Checkbox(text="upx", default=False, key="UPX"), 168 | 169 | ], 170 | [ 171 | 172 | ] 173 | ]), 174 | ], 175 | [ 176 | PySimpleGUI.Frame("需要的数据文件", expand_x=True, layout=[ 177 | [ 178 | PySimpleGUI.Table(values=[[]], headings=["添加列表"], key="DATA-TABLE", 179 | expand_x=True, justification="left")], 180 | [ 181 | PySimpleGUI.Button("添加文件", key="ADD-DATA-FILE"), 182 | PySimpleGUI.Button("添加目录", key="ADD-DATA-DIR"), 183 | PySimpleGUI.Button( 184 | "添加指定模块的所有数据文件", key="ADD-DATA-PACKAGE"), 185 | PySimpleGUI.Button("删除选中", key="DEL-DATA-TABLE")] 186 | ]), 187 | PySimpleGUI.Frame("需要的模块", expand_x=True, layout=[ 188 | [ 189 | PySimpleGUI.Table(values=[[]], headings=["添加列表"], key="MODULE-TABLE", 190 | expand_x=True, justification="left")], 191 | [ 192 | PySimpleGUI.Button("指定需要导入的模块", key="ADD-MODULE-NAME"), 193 | PySimpleGUI.Button("指定不希望导入的模块", key="NO-MODULE-NAME"), 194 | PySimpleGUI.Button("删除选中", key="DEL-MODULE-TABLE")] 195 | ]) 196 | ], 197 | 198 | [ 199 | PySimpleGUI.Text("构建命令:") 200 | ], 201 | [ 202 | PySimpleGUI.Multiline( 203 | key="BUILD-CMD", size=(160, 4), expand_x=True, expand_y=True) 204 | ], 205 | [ 206 | PySimpleGUI.Text("显示输出:"), 207 | 208 | ], 209 | [ 210 | PySimpleGUI.Output(key="BUILD-OUTPUT", 211 | size=(160, 8), expand_x=True, expand_y=True) 212 | ], 213 | [ 214 | PySimpleGUI.Button(button_text="生成构建命令", key="GENERATE-CMD"), 215 | PySimpleGUI.Button(button_text="构建", key="BUILD"), 216 | PySimpleGUI.Button(button_text="退出", key="EXIT") 217 | ] 218 | ] 219 | window = PySimpleGUI.Window(title="psgnuitka", layout=main_layout, font=("", 14), resizable=True, 220 | auto_size_text=True, auto_size_buttons=True, scaling=1.0,) 221 | 222 | data_table = [] 223 | module_table = [] 224 | build_cmd = "" 225 | start_time = 0 226 | stop_time = 0 227 | i = 0 228 | while True: 229 | event, values = window.read(timeout=10, timeout_key="--TIMEOUT--") 230 | if event in [PySimpleGUI.WINDOW_CLOSED, "EXIT"]: 231 | break 232 | if event == "GENERATE-CMD": 233 | cmd = list() 234 | cmd.append(str(pathlib.Path(values['PYTHON-PATH']).absolute())) 235 | cmd.append('-m') 236 | cmd.append('nuitka') 237 | if values["MINGW64"] is True: 238 | cmd.append("--mingw64") 239 | elif values["CLANG"] is True: 240 | cmd.append("--clang") 241 | elif values["MSVC"] is True: 242 | cmd.append("--MSVC") 243 | if values["ONEFILE"] is True: 244 | cmd.append("--onefile") 245 | cmd.append("--standalone") 246 | if values["DISABLE-CONSOLE"] is True: 247 | cmd.append("--disable-console") 248 | if len(module_table) != 0: 249 | for i in module_table: 250 | cmd.append(i[0]) 251 | if len(data_table) != 0: 252 | for i in data_table: 253 | cmd.append(i[0]) 254 | plugin_list = ["eventlet", "gevent", "gi", "glfw", "kivy", "matplotlib", "no-qt", "numpy", "pmw-freezer", 255 | "pylint-warnings", "pyqt5", "pyqt6", "pyside2", "pyside6", "pywebview", "tensorflow", 256 | "tk-inter", "torch", "trio", "upx"] 257 | for plugin in plugin_list: 258 | if values[plugin.upper()] is True: 259 | cmd.append("--enable-plugin=" + plugin) 260 | if values["NUITKATOOLCHAIN"] is True: 261 | cmd.append("--assume-yes-for-downloads") 262 | if values["OUTPUT-FILENAME"]: 263 | cmd.append("--output-filename=" + values["OUTPUT-FILENAME"]) 264 | if values["OUTPUT-DIR"]: 265 | cmd.append("--output-dir=" + 266 | str(pathlib.Path(values["OUTPUT-DIR"]).absolute())) 267 | # cmd.append("--remove-output") 268 | # cmd.append("--verbose") 269 | cmd.append(str(pathlib.Path(values['PY-PATH']).absolute())) 270 | build_cmd = " ".join(cmd) 271 | window['BUILD-CMD'].update(build_cmd) 272 | # print(os.environ.get("PYTHONHOME"), os.environ.get("PYTHONPATH")) 273 | # PySimpleGUI.popup_get_file(message="选择要添加的文件", no_window=True) 274 | if event == "ADD-DATA-FILE": 275 | file_path = PySimpleGUI.popup_get_file( 276 | message="", no_window=True, keep_on_top=True) 277 | if file_path: 278 | file_name = PySimpleGUI.popup_get_text( 279 | message="添加的文件名为", default_text=file_path.split("/")[-1], font="_ 14", keep_on_top=True) 280 | data_table.append( 281 | ["--include-data-files=" + str(pathlib.Path(file_path).absolute()) + "=" + file_name]) 282 | window["DATA-TABLE"].update(values=data_table) 283 | else: 284 | continue 285 | if event == "ADD-DATA-DIR": 286 | dir_path = PySimpleGUI.popup_get_folder( 287 | message="", no_window=True, keep_on_top=True) 288 | if dir_path: 289 | dir_name = PySimpleGUI.popup_get_text(message="添加的目录名为", 290 | default_text=dir_path.split("/")[-1], font="_ 14", keep_on_top=True) 291 | data_table.append( 292 | ["--include-data-dir=" + str(pathlib.Path(dir_path).absolute()) + "=" + dir_name]) 293 | window["DATA-TABLE"].update(values=data_table) 294 | else: 295 | continue 296 | if event == "ADD-DATA-PACKAGE": 297 | package_name = PySimpleGUI.popup_get_text( 298 | message="添加指定模块数据", font="_ 14", keep_on_top=True) 299 | if package_name: 300 | data_table.append(["--include-package-data=" + package_name]) 301 | window["DATA-TABLE"].update(values=data_table) 302 | else: 303 | continue 304 | if event == "DEL-DATA-TABLE": 305 | if values["DATA-TABLE"]: 306 | if len(data_table) != 0: 307 | data_table.pop(values["DATA-TABLE"][0]) 308 | window["DATA-TABLE"].update(values=data_table) 309 | else: 310 | continue 311 | if event == "ADD-MODULE-NAME": 312 | module_name = PySimpleGUI.popup_get_text( 313 | message="希望导入的模块", font="_ 14", keep_on_top=True) 314 | if module_name: 315 | module_table.append(["--follow-import-to=" + module_name]) 316 | window["MODULE-TABLE"].update(values=module_table) 317 | else: 318 | continue 319 | if event == "NO-MODULE-NAME": 320 | module_name = PySimpleGUI.popup_get_text( 321 | message="不希望导入的模块", font="_ 14", keep_on_top=True) 322 | if module_name: 323 | module_table.append(["--nofollow-import-to=" + module_name]) 324 | window["MODULE-TABLE"].update(values=module_table) 325 | else: 326 | continue 327 | if event == "DEL-MODULE-TABLE": 328 | if values["MODULE-TABLE"]: 329 | if len(module_table) != 0: 330 | module_table.pop(values["MODULE-TABLE"][0]) 331 | window["MODULE-TABLE"].update(values=module_table) 332 | else: 333 | continue 334 | if event == "BUILD": 335 | if build_cmd == "": 336 | PySimpleGUI.PopupError( 337 | f"出错了,请先生成构建命令", font="_ 14", keep_on_top=True) 338 | continue 339 | if PySimpleGUI.running_windows(): 340 | os.environ["PYTHONPATH"] = str(pathlib.Path( 341 | values['PYTHON-PATH']).absolute().parent) 342 | os.environ["PYTHONHOME"] = str(pathlib.Path( 343 | values['PYTHON-PATH']).absolute().parent) 344 | start_time = time.perf_counter() 345 | window['BUILD'].update(disabled=True) 346 | window["BUILD-OUTPUT"].update("") 347 | # ret = subprocess.run(build_cmd, shell=True) 348 | # print(build_cmd) 349 | window.perform_long_operation(lambda: runbuildcommand( 350 | command=build_cmd, window=window, system_encode=sys_info['language_code']), "BUILD-DONE") 351 | # stop_time = time.perf_counter() 352 | # use_time = round(stop_time - start_time, 2) 353 | if event == "BUILD-DONE": 354 | stop_time = time.perf_counter() 355 | use_time = round(stop_time - start_time, 2) 356 | # PySimpleGUI.PopupOK(f"处理完毕,用时{use_time}s", font="_ 14") 357 | window['BUILD'].update(disabled=False) 358 | # print(values["BUILD-DONE"]) 359 | if values["BUILD-DONE"][0] == 0: 360 | PySimpleGUI.popup_ok( 361 | f"处理完毕,用时{use_time}s", font="_ 14", keep_on_top=True) 362 | else: 363 | PySimpleGUI.popup_error( 364 | f"出错了,请对照输出检查信息,用时{use_time}s", font="_ 14", keep_on_top=True) 365 | window.close() 366 | --------------------------------------------------------------------------------