diff --git a/README.md b/README.md
index fd22dd2..d1802a5 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,17 @@
-# PyPack
+# QPyPack
-PyPack 是一个为 Python 开发者设计的图形化打包配置工具。它将复杂的命令行打包流程(支持 **PyInstaller**、**Nuitka** 和 **cx_Freeze**)转化为直观、简易的 GUI 界面操作,无需记忆繁琐的终端命令,即可快速生成可执行文件(.exe)。
+QPyPack 是一个为 Python 开发者设计的图形化打包配置工具。它将复杂的命令行打包流程(支持 **PyInstaller**、**Nuitka** 和 **cx_Freeze**)转化为直观、简易的 GUI 界面操作,无需记忆繁琐的终端命令,即可快速生成可执行文件(.exe)。
-
-
+
+
+
---
## 核心设计:让打包变简单
-为了解决传统命令行打包参数多、依赖易冲突、配置繁琐的问题,PyPack 2.0 围绕“易用性”进行了重新设计:
+为了解决传统命令行打包参数多、依赖易冲突、配置繁琐的问题,QPyPack 2.0 围绕“易用性”进行了重新设计:
### 1. 极简的交互流程
* **拖拽即装载**:无需手动输入路径,直接将您的 `.py` 源代码文件拖入软件窗口,即可自动识别并准备构建。
@@ -31,6 +32,14 @@ PyPack 是一个为 Python 开发者设计的图形化打包配置工具。它
---
### Changelog
+2.2
+* 更名为 QPyPack,规范目录结构for PyPI,优化内部沙盒与临时文件命名规范。
+* 超时守护 (Watchdog):引入看门狗机制,为关键打包步骤设置超时限制,防止因网络问题导致构建无限期挂起。
+* 依赖推导优化:pipreqs 原生支持国内镜像站重定向加速,并增加 15 秒超时自愈,失败时自动降级为本地纯静态 AST 解析。
+* 智能命名输出:自动解析脚本版本,预设输出文件名为 {Name}_{Version} 格式(例如:QPyPack_2.2.0.exe)。
+* 编译兼容扩展:Nuitka 编译模式新增对 PyQt6、PySide、NumPy、Matplotlib 等常用插件的自适应识别。
+* 细节打磨:改进日志高频刷新滚动体验,优化版本信息元数据抓取退级逻辑。
+
2.1
* Nuitka单文件打包解压路径修改为%LOCALAPPDATA%
* 增加建构失败动画
@@ -51,11 +60,13 @@ PyPack 是一个为 Python 开发者设计的图形化打包配置工具。它
3. **开始构建**:返回主界面点击“开始构建”,并在下方展开的控制台中实时查看打包进度。
---
-
-## 系统要求
-
-* **支持系统**:Windows (推荐), macOS, Linux
-* **Python 环境**:Python 3.8+
-* **基础依赖**:只需在您的本地系统提前安装好 `PyQt5` 即可运行:
- ```bash
- pip install PyQt5
+### 启动应用
+Python >=3.8 环境中执行以下命令启动程序。
+```bash
+pip install qpypack
+```
+```bash
+qpypack
+```
+### 或者使用打包好的二进制文件直接运行
+[GitHub Releases 下载链接](https://github.com/Qwejay/PyPack/releases)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..8b948d7
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,32 @@
+[build-system]
+requires = ["setuptools>=61.0.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "qpypack"
+version = "2.2.0"
+description = "A modern graphical packaging config tool for PyInstaller, Nuitka, and cx_Freeze."
+readme = "README.md"
+authors = [
+ { name = "QwejayHuang" }
+]
+license = { text = "MIT" }
+requires-python = ">=3.8"
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: Microsoft :: Windows",
+]
+
+dependencies = [
+ "PyQt5>=5.15.0",
+]
+
+[project.urls]
+Homepage = "https://github.com/qwejay/QPyPack"
+
+[project.scripts]
+qpypack = "qpypack.main:main"
+
+[tool.setuptools.package-data]
+qpypack = ["icon.ico"]
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..06e3fa7
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+PyQt5==5.15.11
+pyqt5_sip==12.18.0
diff --git a/src/qpypack/__init__.py b/src/qpypack/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/icon.ico b/src/qpypack/icon.ico
similarity index 100%
rename from icon.ico
rename to src/qpypack/icon.ico
diff --git a/main.py b/src/qpypack/main.py
similarity index 90%
rename from main.py
rename to src/qpypack/main.py
index 26b4323..da3e728 100644
--- a/main.py
+++ b/src/qpypack/main.py
@@ -1,9 +1,11 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-"""
-PyPack 2.1 - 自动化构建工具
-支持引擎: PyInstaller / Nuitka / cx_Freeze
-"""
+
+__app_name__ = "QPyPack"
+__version__ = "2.2.0"
+__author__ = "QwejayHuang"
+__company__ = "Qwesoft"
+__description__ = "自动化 Python 脚本打包构建工具"
import sys
import os
@@ -16,6 +18,7 @@ import stat
import ast
import locale
import math
+import threading
from pathlib import Path
os.environ["QT_LOGGING_RULES"] = "qt.text.font.db=false"
@@ -32,7 +35,6 @@ from PyQt5.QtCore import Qt, QThread, pyqtSignal, QPropertyAnimation, QEasingCur
from PyQt5.QtGui import QFont, QDragEnterEvent, QDropEvent, QTextCursor, QIcon, QPixmap, QPainter, QColor, QPen
from PyQt5.QtSvg import QSvgRenderer
-# ======================== Material Design 内置 SVG 引擎 ========================
MATERIAL_ICONS = {
'settings': 'M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.73,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.43-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.49-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z',
'refresh': 'M17.65,6.35C16.2,4.9,14.21,4,12,4c-4.42,0-7.99,3.58-7.99,8s3.57,8,7.99,8c3.73,0,6.84-2.55,7.73-6h-2.08 c-0.82,2.33-3.04,4-5.65,4c-3.31,0-6-2.69-6-6s2.69-6,6-6c1.66,0,3.14,0.69,4.22,1.78L13,11h7V4L17.65,6.35z',
@@ -65,7 +67,6 @@ def get_svg_pixmap(name, color="#5F6368", size=64):
return get_svg_icon(name, color, size).pixmap(size, size)
-# ======================== 工具与配置 ========================
def get_stdlib_names():
libs = {'os', 'sys', 're', 'math', 'time', 'datetime', 'json', 'urllib', 'sqlite3', 'csv',
'subprocess', 'shutil', 'threading', 'multprocessing', 'queue', 'socket',
@@ -106,7 +107,6 @@ def robust_rmtree(path: Path, retries=15, delay=0.8):
return False
-# ======================== 界面动效组件 ========================
class AnimatedButton(QPushButton):
def __init__(self, text, parent=None):
super().__init__(text, parent)
@@ -148,13 +148,12 @@ class AnimatedButton(QPushButton):
super().leaveEvent(event)
class TargetIconWidget(QWidget):
- """自定义图标渲染组件,接管无损抗锯齿与复合重绘效果"""
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(200, 200)
self.pixmap = None
self.base_pixmap = None
- self.file_pixmap = None # 用于专门记住当前加载文件的专属图标
+ self.file_pixmap = None
self.current_size = 88
self.is_building = False
@@ -180,7 +179,6 @@ class TargetIconWidget(QWidget):
self.burst_anim.setEasingCurve(QEasingCurve.OutQuad)
self.burst_anim.valueChanged.connect(self._animate_burst)
- # 抖动动画配置(用于构建失败)
self.shake_offset = 0
self.shake_anim = QVariantAnimation(self)
self.shake_anim.setDuration(500)
@@ -213,7 +211,6 @@ class TargetIconWidget(QWidget):
self.update()
def start_building(self):
- # 恢复之前保存的文件专属图标,覆盖掉可能残留的“成功”或“失败”图标
if getattr(self, 'file_pixmap', None) and not self.file_pixmap.isNull():
self.pixmap = self.file_pixmap
self.current_size = 88
@@ -242,7 +239,7 @@ class TargetIconWidget(QWidget):
def start_failure(self):
self.stop_building()
self.success_effect.setBlurRadius(40)
- self.success_effect.setColor(QColor(217, 48, 37, 180)) # 红色警告发光
+ self.success_effect.setColor(QColor(217, 48, 37, 180))
self.shake_anim.start()
def reset(self):
@@ -252,7 +249,7 @@ class TargetIconWidget(QWidget):
self.burst_value = 0.0
self.shake_offset = 0
self.success_effect.setColor(QColor(0, 0, 0, 0))
- self.file_pixmap = None # 清空文件专属图标
+ self.file_pixmap = None
self.pixmap = self.base_pixmap
self.current_size = 88
self.update()
@@ -416,7 +413,6 @@ class DropArea(QFrame):
if not pixmap:
pixmap = get_svg_pixmap('package', color="#1A73E8", size=88)
- # 使用 set_file_pixmap,避免覆盖 base_pixmap 导致复位失败
self.icon_widget.set_file_pixmap(pixmap, 88)
self.label.setText(f"已加载:{filename}")
@@ -766,7 +762,7 @@ class SettingsPanel(QWidget):
if not script_path: return QMessageBox.warning(self, "提示", "请先在主界面加载脚本!")
try:
raw = Path(script_path).read_bytes()
- if b"__CLOUDSYNC_ENC__" in raw[:1024]: return QMessageBox.critical(self, "错误", "脚本被云盘加密,请解密!")
+ if b"__CLOUDSYNC_ENC__" in raw[:1024]: return QMessageBox.warning(self, "错误", "脚本被云盘加密,请解密!")
try: code = raw.decode('utf-8-sig')
except: code = raw.decode('gbk', errors='ignore')
@@ -784,7 +780,7 @@ class SettingsPanel(QWidget):
hidden = [m for m in hidden if m not in STD_LIBS]
self.hidden_edit.setText(','.join(hidden))
QMessageBox.information(self, "扫描完成", f"捕获到 {len(hidden)} 项依赖。")
- except Exception as e: QMessageBox.critical(self, "错误", f"扫描失败: {e}")
+ except Exception as e: QMessageBox.warning(self, "错误", f"扫描失败: {e}")
def add_resource(self):
choice = QMessageBox.question(self, "添加资源", "Yes=文件, No=文件夹", QMessageBox.Yes | QMessageBox.No)
@@ -832,13 +828,32 @@ class PackingThread(QThread):
else: self.process.kill()
except: pass
- def run_cmd(self, cmd, cwd=None):
+ def run_cmd(self, cmd, cwd=None, timeout=None):
if self._is_cancelled: return False
+
+ timer = None
+ is_timeout = [False]
+
try:
kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.STDOUT, "cwd": cwd, "text": True, "errors": "replace"}
if os.name == 'nt': kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
self.process = subprocess.Popen(cmd, **kwargs)
+
+ if timeout:
+ def kill_proc():
+ is_timeout[0] = True
+ try:
+ if os.name == "nt":
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(self.process.pid)],
+ capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW)
+ else:
+ self.process.kill()
+ except:
+ pass
+ timer = threading.Timer(timeout, kill_proc)
+ timer.start()
+
for line in self.process.stdout:
if self._is_cancelled:
self.process.terminate()
@@ -846,10 +861,18 @@ class PackingThread(QThread):
self.progress.emit(line.strip())
self.process.wait()
+
+ if is_timeout[0]:
+ self.progress.emit(f"[提示] 该步骤执行超时 ({timeout}秒),已自动跳过并进入下一步...")
+ return False
+
return self.process.returncode == 0
except Exception as e:
self.progress.emit(f"[系统异常] 进程执行失败: {e}")
return False
+ finally:
+ if timer:
+ timer.cancel()
def sanitize_script(self, orig_path: Path):
try:
@@ -858,8 +881,6 @@ class PackingThread(QThread):
return None, False, "目标脚本被云盘锁定加密!"
except Exception:
pass
- # 移除了所有重命名及生成 pypack_temp_ 前缀文件的死代码
- # 直接透传原始文件可防止 Nuitka 生成带有临时前缀的 DLL 触发 Windows 智能应用控制的启发式拦截
return orig_path, False, ""
def run(self):
@@ -884,7 +905,7 @@ class PackingThread(QThread):
if self.params['use_venv']:
self.progress.emit("[环境] 初始化虚拟隔离沙盒...")
- self.venv_dir = Path(tempfile.mkdtemp(prefix="pypack_env_")).resolve()
+ self.venv_dir = Path(tempfile.mkdtemp(prefix="qpypack_env_")).resolve()
if not self.run_cmd([get_python_executable(), "-m", "venv", self.venv_dir.as_posix()]):
return self.finished.emit(False, "虚拟环境初始化失败。")
python_exe = (self.venv_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python")).as_posix()
@@ -899,6 +920,8 @@ class PackingThread(QThread):
self.run_cmd([python_exe, "-m", "pip", "install", "-q"] + core_pkgs + pip_args)
req_success = False
+
+ # 1. 优先尝试部署 requirements.txt
if self.params.get('use_reqs'):
req_file = script_dir / "requirements.txt"
if req_file.exists():
@@ -914,16 +937,43 @@ class PackingThread(QThread):
req_success = True
temp_req.unlink(missing_ok=True)
except Exception as e: self.progress.emit(f"[警告] {e}")
+
+ # 2. requirements.txt 未能成功解决依赖,则启用 pipreqs(带镜像站原生参数及 15 秒超时自愈)
if self.params.get('use_pipreqs') and not req_success:
self.progress.emit("[依赖] 执行 pipreqs 深度推导...")
self.run_cmd([python_exe, "-m", "pip", "install", "pipreqs", "-q"] + pip_args)
temp_pipreqs = script_dir / "pypack_pipreqs_out.txt"
- self.run_cmd([python_exe, "-m", "pipreqs.pipreqs", script_dir.as_posix(), "--encoding", "utf-8", "--force", "--savepath", temp_pipreqs.as_posix()])
- if temp_pipreqs.exists():
- if self.run_cmd([python_exe, "-m", "pip", "install", "-q", "-r", temp_pipreqs.as_posix()] + pip_args): req_success = True
- temp_pipreqs.unlink(missing_ok=True)
+
+ pypi_server = None
+ if pip_idx:
+ pypi_server = re.sub(r'/simple/?$', '/pypi', pip_idx.strip(), flags=re.I).rstrip('/')
+
+ pipreqs_cmd = [
+ python_exe, "-m", "pipreqs.pipreqs",
+ script_dir.as_posix(),
+ "--encoding", "utf-8",
+ "--force",
+ "--savepath", temp_pipreqs.as_posix()
+ ]
+
+ if pypi_server:
+ pipreqs_cmd.extend(["--pypi-server", pypi_server])
+ self.progress.emit(f"[依赖] pipreqs 已重定向至本地镜像接口: {pypi_server}")
+
+ if self.run_cmd(pipreqs_cmd, timeout=15):
+ if temp_pipreqs.exists():
+ if self.run_cmd([python_exe, "-m", "pip", "install", "-q", "-r", temp_pipreqs.as_posix()] + pip_args):
+ req_success = True
+ temp_pipreqs.unlink(missing_ok=True)
+ else:
+ self.progress.emit("[提示] pipreqs 深度解析超时或失败,转为本地 AST 解析...")
+ if temp_pipreqs.exists():
+ try: temp_pipreqs.unlink()
+ except: pass
+
+ # 3. 如果以上方案均未成功解决依赖,则采用本地纯静态 AST 降级解析兜底
if not req_success and (self.params.get('use_reqs') or self.params.get('use_pipreqs')):
- self.progress.emit("[依赖] 执行 AST 语法树降级解析...")
+ self.progress.emit("[依赖] 正在启用 AST 本地语法树解析机制兜底...")
try:
code = build_script_path.read_text(encoding='utf-8', errors='ignore')
third_party = set()
@@ -949,18 +999,17 @@ class PackingThread(QThread):
icon_path = Path(self.params['icon']).resolve().as_posix() if self.params.get('icon') else None
if engine == "PyInstaller":
- self.temp_workpath = Path(tempfile.mkdtemp(prefix="pypack_build_")).resolve()
+ self.temp_workpath = Path(tempfile.mkdtemp(prefix="qpypack_build_")).resolve()
cmd = [python_exe, "-m", "PyInstaller", "--clean", "-y", f"--workpath={self.temp_workpath.as_posix()}", f"--name={app_name}"]
+
if self.params.get('onefile'):
cmd.append("--onefile")
- if os.name == "nt":
- cmd.append(f"--windows-onefile-tempdir-spec=%LOCALAPPDATA%\\{app_name}_Env")
else:
- cmd.append("--standalone")
+ cmd.append("--onedir")
- if self.params.get('noconsole'): cmd.append("--windows-console-mode=disable")
+ if self.params.get('noconsole'):
+ cmd.append("--noconsole")
-
if icon_path:
cmd.extend(["--icon", icon_path])
sep = ';' if os.name == 'nt' else ':'
@@ -998,7 +1047,6 @@ class PackingThread(QThread):
cmd.append(f"--windows-icon-from-ico={icon_path}")
elif sys.platform == "darwin" and icon_path.endswith(".icns"):
cmd.append(f"--macos-app-icon={icon_path}")
- # 使用精确的文件名作为目标部署位置,以避开 Nuitka '.' 校验致命错误
cmd.append(f"--include-data-files={icon_path}={Path(icon_path).name}")
if os.name == "nt":
@@ -1007,8 +1055,19 @@ class PackingThread(QThread):
if self.params.get('ver_ver'): cmd.append(f"--file-version={self.params['ver_ver']}")
try:
- if 'PyQt5' in build_script_path.read_text(encoding='utf-8', errors='ignore'):
+ script_text = build_script_path.read_text(encoding='utf-8', errors='ignore')
+ if 'PyQt5' in script_text:
cmd.append("--enable-plugin=pyqt5")
+ elif 'PyQt6' in script_text:
+ cmd.append("--enable-plugin=pyqt6")
+ elif 'PySide2' in script_text:
+ cmd.append("--enable-plugin=pyside2")
+ elif 'PySide6' in script_text:
+ cmd.append("--enable-plugin=pyside6")
+ elif 'numpy' in script_text:
+ cmd.append("--enable-plugin=numpy")
+ elif 'matplotlib' in script_text:
+ cmd.append("--enable-plugin=matplotlib")
except: pass
for imp in self.params.get('hidden_imports', '').split(','):
@@ -1077,7 +1136,7 @@ class PackingThread(QThread):
self.progress.emit("[清理] 抹除环境与沙盒...")
for p in [self.venv_dir, self.temp_workpath, self.temp_out_dir]:
if p and p.exists(): robust_rmtree(p)
-
+
cwd = Path.cwd().resolve()
app_name = self.params.get('app_name', 'app')
robust_rmtree(cwd / "dist")
@@ -1098,7 +1157,8 @@ class MainWindow(QMainWindow):
self.init_ui()
def init_style(self):
- self.setWindowTitle("PyPack 2.1")
+ self.setWindowTitle(f"{__app_name__} {__version__} - {__author__}")
+
self.setMinimumSize(580, 520)
self.resize(600, 560)
@@ -1280,8 +1340,49 @@ class MainWindow(QMainWindow):
except: pass
self.script_path = path
- self.settings_panel.name_edit.setText(Path(path).stem)
+ # 智能元数据预设逻辑
+ app_name = Path(path).stem
+ version = ""
+ author = "My Studio"
+ desc = "Python Executable"
+
+ try:
+ content = Path(path).read_text(encoding='utf-8', errors='ignore')
+
+ # 解析版本
+ v_match = re.search(r'^(?:__version__|VERSION|version)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I)
+ if v_match:
+ version = v_match.group(1)
+ self.settings_panel.ver_ver.setText(version)
+
+ # 解析发行公司(无company则降级尝试绑定author)
+ c_match = re.search(r'^(?:__company__|COMPANY)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I)
+ if c_match:
+ author = c_match.group(1)
+ else:
+ a_match = re.search(r'^(?:__author__|AUTHOR)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I)
+ if a_match: author = a_match.group(1)
+ self.settings_panel.ver_comp.setText(author)
+
+ # 解析名称
+ n_match = re.search(r'^(?:__title__|__app_name__|APP_NAME)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I)
+ if n_match:
+ app_name = n_match.group(1)
+
+ # 解析描述
+ d_match = re.search(r'^(?:__description__|DESCRIPTION)\s*=\s*[\'"]([^\'"]+)[\'"]', content, re.M | re.I)
+ if d_match:
+ desc = d_match.group(1)
+ self.settings_panel.ver_desc.setText(desc)
+
+ except: pass
+
+ # 拼装生成带版本后缀的默认文件名
+ default_output_name = f"{app_name}_{version}" if version else app_name
+ self.settings_panel.name_edit.setText(default_output_name)
+
+ # 图标自动配对
script_dir = Path(path).parent
auto_icon = None
@@ -1322,8 +1423,12 @@ class MainWindow(QMainWindow):
version_file = None
if engine == "PyInstaller" and sp.ver_ver.text().strip():
try:
- content = f'''VSVersionInfo(ffi=FixedFileInfo(filevers=(1,0,0,0),prodvers=(1,0,0,0),mask=0x3f,flags=0x0,OS=0x40004,fileType=0x1,subtype=0x0,date=(0,0)),kids=[StringFileInfo([StringTable('040904B0',[StringStruct('CompanyName','{sp.ver_comp.text()}'),StringStruct('FileDescription','{sp.ver_desc.text()}'),StringStruct('FileVersion','{sp.ver_ver.text()}'),StringStruct('ProductVersion','{sp.ver_ver.text()}'),StringStruct('OriginalFilename','{app_name}.exe')])])])'''
- version_file = Path(tempfile.gettempdir()) / f"pypack_{app_name}_version.txt"
+ v_str = sp.ver_ver.text().strip()
+ v_nums = re.findall(r'\d+', v_str)
+ v_tuple = ",".join((v_nums + ['0', '0', '0', '0'])[:4])
+
+ content = f'''VSVersionInfo(ffi=FixedFileInfo(filevers=({v_tuple}),prodvers=({v_tuple}),mask=0x3f,flags=0x0,OS=0x40004,fileType=0x1,subtype=0x0,date=(0,0)),kids=[StringFileInfo([StringTable('040904B0',[StringStruct('CompanyName','{sp.ver_comp.text()}'),StringStruct('FileDescription','{sp.ver_desc.text()}'),StringStruct('FileVersion','{v_str}'),StringStruct('ProductVersion','{v_str}'),StringStruct('OriginalFilename','{app_name}.exe')])])])'''
+ version_file = Path(tempfile.gettempdir()) / f"qpypack_{app_name}_version.txt"
version_file.write_text(content, encoding='utf-8')
except: pass
@@ -1398,10 +1503,10 @@ class MainWindow(QMainWindow):
def append_log(self, msg):
self.log.append(msg)
- self.log.moveCursor(QTextCursor.End)
+ self.log.ensureCursorVisible()
-if __name__ == "__main__":
+def main():
if hasattr(Qt, 'HighDpiScaleFactorRoundingPolicy'):
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
@@ -1413,3 +1518,6 @@ if __name__ == "__main__":
win = MainWindow()
win.show()
sys.exit(app.exec_())
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file