v2.7.9: Enhance build diagnostics, fix keep_venv config load, and restore Nuitka --jobs
- feat(log): add build diagnostics header and [DETAILED_ONLY] log routing tag - feat(log): capture full exception traceback on fatal build failures - feat(venv): add timestamped rename fallback and manual prompt for invalid venvs - feat(build): implement adaptive RAM concurrency tuning and --low-memory mode for Nuitka - fix(config): fix keep_venv checkbox state reset bug during config loading - fix(nuitka): restore missing --jobs parameter in Nuitka build command - fix(app): optimize suffix stripping logic for clean_product_name - i18n: update ZH_CN dictionary and complete language dynamic refresh bindings
This commit is contained in:
1 parent
fb6fa0aa86
commit
06ca9975f0
4 files changed
+109
-24
No files matched your search
@@ -13,6 +13,20 @@ QPyPack 项目的所有重要变更都将记录在此文件中。
|
||||
|
||||
---
|
||||
|
||||
## [2.7.9] - 2026-08-12
|
||||
|
||||
#### 新增
|
||||
- **环境诊断与堆栈隔离**:新增打包诊断面板与崩溃 Traceback 捕获,并通过 `[DETAILED_ONLY]` 标签将详细调试信息隔离至详细日志,保持简略日志整洁。
|
||||
- **虚拟环境故障容错**:清除失效环境失败且保留配置时,自动尝试重命名备份;若重命名失败则提供精准的手动处理指引。
|
||||
- **内存自适应并发控制**:根据物理空闲内存动态调整 Nuitka 并发核心数,并在低内存环境下自动追加 `--low-memory` 参数。
|
||||
|
||||
#### 修复
|
||||
- **设置配置加载修复**:修复加载配置时“保留本地虚拟环境”复选框状态被错误重置为 `False` 的问题。
|
||||
- **Nuitka 参数遗漏修复**:补回 Nuitka 打包命令中遗漏的多核并发编译参数。
|
||||
- **产品名称清理优化**:优化去除版本后缀时的字符串匹配逻辑,防止提取应用名称时发生误删或边界异常。
|
||||
|
||||
---
|
||||
|
||||
## [2.7.8] - 2026-08-11
|
||||
|
||||
#### 新增
|
||||
|
||||
+15
-1
@@ -9,7 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [2.7.8] - 2026-08-10
|
||||
## [2.7.9] - 2026-08-12
|
||||
|
||||
#### Added
|
||||
- **Build Diagnostics & Stack Tracing**: Added build environment diagnostics header and fatal traceback capturing, with a new `[DETAILED_ONLY]` log routing tag to keep concise logs clean.
|
||||
- **Venv Fault-Tolerance**: Automatically renames invalid virtual environments as backups when cleanup fails, providing clear manual instructions if backup fails.
|
||||
- **Adaptive Memory Concurrency**: Dynamically adjusts Nuitka `--jobs` concurrency based on free physical memory and automatically enables `--low-memory` flag under memory pressure.
|
||||
|
||||
#### Fixed
|
||||
- **Config Load Bug**: Fixed an issue where the "Keep Local Venv" (`keep_venv`) setting was mistakenly reset to `False` when loading configurations.
|
||||
- **Missing Nuitka Flags**: Restored the missing `--jobs` multi-core compilation parameter in Nuitka build commands.
|
||||
- **Product Name Truncation**: Improved string suffix matching when stripping version tags from app names to prevent accidental truncation or slicing errors.
|
||||
|
||||
---
|
||||
|
||||
## [2.7.8] - 2026-08-11
|
||||
|
||||
#### Added
|
||||
- **Asynchronous Background Venv Cleanup**: Introduced a background worker thread to offload scanning and deletion of local virtual environments from the UI thread, preventing interface freezing during large folder cleanups while accurately calculating and displaying freed disk space.
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "qpypack"
|
||||
version = "2.7.8"
|
||||
version = "2.7.9"
|
||||
description = "A cross-platform GUI packaging and compilation suite based on PyInstaller and Nuitka."
|
||||
readme = "README_EN.md"
|
||||
authors = [
|
||||
|
||||
+79
-22
@@ -61,7 +61,7 @@ except ImportError:
|
||||
HAS_QT_AUDIO = False
|
||||
|
||||
__app_name__ = "QPyPack"
|
||||
__version__ = "2.7.8"
|
||||
__version__ = "2.7.9"
|
||||
__author__ = "QwejayHuang"
|
||||
__company__ = "QwejayHuang"
|
||||
__description__ = "Modern Cross-Platform Python Packaging GUI Powered by PyInstaller & Nuitka"
|
||||
@@ -478,6 +478,9 @@ ZH_CN_DICT = {
|
||||
"Package Name": "包名称",
|
||||
"[ERROR] Environment remediation failed: {error}": "[ERROR] 环境修复失败:{error}",
|
||||
"[INFO] Auto-shielded obsolete backport module: '{pkg}' (Built-in in Python {ver})": "[INFO] 已自动屏蔽过时的反向移植模块:'{pkg}'(Python {ver} 内置)",
|
||||
"[WARN] Failed to delete invalid venv, attempting to rename: {error}": "[WARN] 无法删除失效虚拟环境,尝试重命名: {error}",
|
||||
"[INFO] Old venv renamed to: {name}": "[INFO] 旧虚拟环境已重命名为: {name}",
|
||||
"[ERROR] Unable to clean up invalid virtual environment:\n Path: {path}\n Error: {error}\n\nPlease manually delete the directory or uncheck 'Keep Local Venv' and retry.": "[ERROR] 无法清理失效的虚拟环境:\n 路径: {path}\n 错误: {error}\n\n请手动删除该目录,或取消勾选'保留本地虚拟环境'后重试。",
|
||||
"Unknown": "未知"
|
||||
}
|
||||
|
||||
@@ -2800,7 +2803,6 @@ class SettingsPanel(QWidget):
|
||||
btn_map_lay.addStretch()
|
||||
c_lay_map.addLayout(btn_map_lay)
|
||||
|
||||
# --- 新增卡片:废弃兼容包拦截规则 ---
|
||||
self.card_rules, c_lay_rules = self._create_card(_("Obsolete Backport Exclusion Rules"))
|
||||
|
||||
self.enable_shield_check = QCheckBox(_("Enable Automatic Backport Isolation"))
|
||||
@@ -3228,7 +3230,6 @@ class SettingsPanel(QWidget):
|
||||
self.lbl_res_hint.setText(_("Project-specific. Not saved to global preferences. Use 'Export Preset' to save config."))
|
||||
self.add_data_list.setToolTip(_("Double-click to edit target path; Drag & drop supported. Use 'Export Preset' to save for reuse."))
|
||||
|
||||
# --- 补充遗漏的语言刷新控件 ---
|
||||
self.btn_clean_venvs.setText(_("Clear Local Venvs"))
|
||||
self.enable_sign_check.setText(_("Enable Smart Code Signing"))
|
||||
self.enable_sign_check.setToolTip(_("Auto-applies digital signature to built app, eliminating 'Unknown Publisher' warning"))
|
||||
@@ -3614,7 +3615,7 @@ class SettingsPanel(QWidget):
|
||||
|
||||
self.venv_check.setChecked(s.getboolean('use_venv', True))
|
||||
|
||||
self.keep_venv_check.setChecked(False)
|
||||
self.keep_venv_check.setChecked(s.getboolean('keep_venv', False))
|
||||
self.keep_venv_check.setEnabled(self.venv_check.isChecked())
|
||||
|
||||
self.reqs_check.setChecked(s.getboolean('use_reqs', True))
|
||||
@@ -4577,8 +4578,28 @@ class PackingThread(QThread):
|
||||
if self._is_cancelled:
|
||||
return self.build_finished.emit(False, _("[INFO] Build Cancelled."), [])
|
||||
|
||||
try:
|
||||
import platform
|
||||
os_info = f"{platform.system()} {platform.release()} ({platform.machine()})"
|
||||
py_arch = "64-bit" if sys.maxsize > 2**32 else "32-bit"
|
||||
diag_msg = (
|
||||
"[DETAILED_ONLY]\n"
|
||||
"=== QPyPack Build Diagnostics ===\n"
|
||||
f"QPyPack Ver : {__version__}\n"
|
||||
f"OS Platform : {os_info}\n"
|
||||
f"Host Python : {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} ({py_arch}) - {sys.executable}\n"
|
||||
f"Target Py : {self.params.get('python_exe')}\n"
|
||||
f"Build Engine: {engine} (OneFile: {self.params['onefile']}, NoConsole: {self.params['noconsole']}, LiteMode: {self.params.get('lite_mode')})\n"
|
||||
f"Workspace : {script_dir.as_posix()}\n"
|
||||
"================================="
|
||||
)
|
||||
self.progress.emit(diag_msg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.progress.emit(_("[INFO] Analyzing source code and project dependencies..."))
|
||||
script_path = Path(self.params['script_path']).resolve()
|
||||
|
||||
script_dir = script_path.parent
|
||||
|
||||
try:
|
||||
@@ -4777,9 +4798,27 @@ class PackingThread(QThread):
|
||||
try:
|
||||
robust_rmtree(self.venv_dir)
|
||||
except Exception as e:
|
||||
self.progress.emit(_("[WARN] Failed to purge invalid environment, falling back to temp sandbox: {error}", error=str(e)))
|
||||
self.venv_dir = Path(tempfile.mkdtemp(prefix=f"qpypack_env_{short_stem}_", dir=temp_dir_arg)).resolve()
|
||||
python_exe_in_venv = (self.venv_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python")).as_posix()
|
||||
if self.params.get('keep_venv'):
|
||||
self.progress.emit(_("[WARN] Failed to delete invalid venv, attempting to rename: {error}", error=str(e)))
|
||||
try:
|
||||
backup_name = f"{self.venv_dir.name}_backup_{int(time.time())}"
|
||||
backup_path = self.venv_dir.parent / backup_name
|
||||
self.venv_dir.rename(backup_path)
|
||||
self.progress.emit(_("[INFO] Old venv renamed to: {name}", name=backup_name))
|
||||
except Exception as rename_err:
|
||||
error_msg = _(
|
||||
"[ERROR] Unable to clean up invalid virtual environment:\n"
|
||||
" Path: {path}\n"
|
||||
" Error: {error}\n\n"
|
||||
"Please manually delete the directory or uncheck 'Keep Local Venv' and retry.",
|
||||
path=self.venv_dir.as_posix(),
|
||||
error=str(rename_err)
|
||||
)
|
||||
return self.build_finished.emit(False, error_msg, [])
|
||||
else:
|
||||
self.progress.emit(_("[WARN] Failed to purge invalid environment, falling back to temp sandbox: {error}", error=str(e)))
|
||||
self.venv_dir = Path(tempfile.mkdtemp(prefix=f"qpypack_env_{short_stem}_", dir=temp_dir_arg)).resolve()
|
||||
python_exe_in_venv = (self.venv_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python")).as_posix()
|
||||
|
||||
self.progress.emit(_("[INFO] Creating virtual environment ({name})...", name=self.venv_dir.name))
|
||||
if not self.run_cmd([system_python_exe, "-m", "venv", self.venv_dir.as_posix()]):
|
||||
@@ -5147,13 +5186,24 @@ class PackingThread(QThread):
|
||||
|
||||
cores = self.params.get('cpu_cores', os.cpu_count() or 2)
|
||||
free_ram = get_free_ram_gb()
|
||||
if free_ram < 4.0 and cores > 2:
|
||||
safe_jobs = max(1, min(cores, int(free_ram / 1.5)))
|
||||
|
||||
if free_ram < 2.0 and cores > 1:
|
||||
safe_jobs = 1
|
||||
self.progress.emit(_("[INFO] Evaluating system physical memory (Available: {ram:.1f} GB). Adaptive concurrency adjusted: {cores} -> {safe_jobs} ...", ram=free_ram, cores=cores, safe_jobs=safe_jobs))
|
||||
cores = safe_jobs
|
||||
cmd.append("--low-memory")
|
||||
elif free_ram < 3.5 and cores > 2:
|
||||
safe_jobs = max(2, min(cores, int(free_ram)))
|
||||
self.progress.emit(_("[INFO] Evaluating system physical memory (Available: {ram:.1f} GB). Adaptive concurrency adjusted: {cores} -> {safe_jobs} ...", ram=free_ram, cores=cores, safe_jobs=safe_jobs))
|
||||
cores = safe_jobs
|
||||
cmd.append("--low-memory")
|
||||
elif free_ram < 6.0 and cores > 4:
|
||||
safe_jobs = max(3, min(cores, int(free_ram / 1.5)))
|
||||
if safe_jobs < cores:
|
||||
self.progress.emit(_("[INFO] Evaluating system physical memory (Available: {ram:.1f} GB). Adaptive concurrency adjusted: {cores} -> {safe_jobs} ...", ram=free_ram, cores=cores, safe_jobs=safe_jobs))
|
||||
cores = safe_jobs
|
||||
cmd.append(f"--jobs={cores}")
|
||||
|
||||
|
||||
if self.params.get('upx'):
|
||||
cmd.append("--enable-plugin=upx")
|
||||
upx_dir_custom = (self.params.get('upx_path') or '').strip()
|
||||
@@ -5271,14 +5321,6 @@ class PackingThread(QThread):
|
||||
|
||||
cmd.append(script_posix)
|
||||
|
||||
if enable_shield and active_exclusions:
|
||||
if engine == "PyInstaller":
|
||||
for bad_pkg in active_exclusions:
|
||||
cmd.extend(["--exclude-module", bad_pkg])
|
||||
elif engine == "Nuitka":
|
||||
for bad_pkg in active_exclusions:
|
||||
cmd.append(f"--nofollow-import-to={bad_pkg}")
|
||||
|
||||
if self._is_cancelled:
|
||||
return self.build_finished.emit(False, _("[INFO] Build Cancelled."), [])
|
||||
|
||||
@@ -5432,7 +5474,9 @@ class PackingThread(QThread):
|
||||
except Exception as e:
|
||||
if self._is_cancelled:
|
||||
return self.build_finished.emit(False, _("[INFO] Build Cancelled."), [])
|
||||
self.build_finished.emit(False, f"[ERROR] {str(e)}", failed_packages)
|
||||
err_trace = traceback.format_exc()
|
||||
self.progress.emit(f"[DETAILED_ONLY]\n[FATAL ERROR TRACE]\n{err_trace}\n")
|
||||
self.build_finished.emit(False, f"[ERROR] {str(e)}\n\n(See Detailed Log for complete trace)", failed_packages)
|
||||
finally:
|
||||
if "PYTHONPATH" in os.environ and "_qpypack_temp_" in os.environ.get("PYTHONPATH", ""):
|
||||
parts = os.environ["PYTHONPATH"].split(os.pathsep)
|
||||
@@ -5479,7 +5523,11 @@ class PackingThread(QThread):
|
||||
except: pass
|
||||
|
||||
if sandbox_mode == 0 and custom_temp_base and custom_temp_base.exists():
|
||||
robust_rmtree(custom_temp_base)
|
||||
try:
|
||||
if not any(custom_temp_base.iterdir()):
|
||||
custom_temp_base.rmdir()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
class PythonInstallMonitorThread(QThread):
|
||||
finished_signal = Signal(bool)
|
||||
@@ -6442,8 +6490,11 @@ class MainWindow(QMainWindow):
|
||||
v_str_escaped = v_str.replace("\\", "\\\\").replace("'", "\\'")
|
||||
|
||||
clean_product_name = app_name
|
||||
if v_str and clean_product_name.endswith(f"_{v_str}"):
|
||||
clean_product_name = clean_product_name[:-len(f"_{v_str}")]
|
||||
if v_str:
|
||||
suffix = f"_{v_str}"
|
||||
if clean_product_name.endswith(suffix):
|
||||
clean_product_name = clean_product_name[:-len(suffix)]
|
||||
|
||||
|
||||
app_name_escaped = clean_product_name.replace("\\", "\\\\").replace("'", "\\'")
|
||||
orig_filename_escaped = app_name.replace("\\", "\\\\").replace("'", "\\'")
|
||||
@@ -6646,10 +6697,16 @@ class MainWindow(QMainWindow):
|
||||
self.log_stack.setCurrentWidget(self.log_concise if is_concise else self.log_detailed)
|
||||
|
||||
def append_log(self, msg, is_error=False):
|
||||
if msg.startswith("[DETAILED_ONLY]"):
|
||||
clean_msg = msg.replace("[DETAILED_ONLY]", "", 1).lstrip('\r\n')
|
||||
self._render_text_edit(self.log_detailed, clean_msg, is_error)
|
||||
return
|
||||
|
||||
concise_lines = []
|
||||
|
||||
for line in msg.split('\n'):
|
||||
line_strip = line.strip()
|
||||
|
||||
if not line_strip: continue
|
||||
|
||||
if is_error:
|
||||
|
||||
Reference in new issue
Block a user