Skip to content

Conversation

@WSXYT
Copy link
Collaborator

@WSXYT WSXYT commented Jan 11, 2026

开始打包

Copilot AI review requested due to automatic review settings January 11, 2026 17:06
@WSXYT WSXYT merged commit 750e821 into SECTL:master Jan 11, 2026
8 of 10 checks passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request transitions the application's packaging and update mechanism from Windows EXE installers to cross-platform ZIP archives and Linux DEB packages. The changes enable Linux support and modify the update installation process to handle the new packaging formats.

Changes:

  • Modified packaging format from EXE to ZIP/DEB with updated metadata configuration
  • Refactored update installation logic to support ZIP extraction and cross-platform updates
  • Updated CI/CD workflow to build ZIP packages for both Windows and Linux

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 13 comments.

File Description
metadata.yaml Updated package format specification, added Linux support, changed naming convention to support multiple platforms and package types
app/tools/update_utils.py Replaced EXE integrity checking with ZIP/DEB validation, added cross-platform extraction logic, implemented embedded installer script for production updates
.github/workflows/build-unified.yml Removed Inno Setup language file installation, added ZIP compression step, updated release changelog generation for new package formats
Comments suppressed due to low confidence (1)

.github/workflows/build-unified.yml:204

  • The Inno Setup packaging step is still present in the workflow (lines 182-204), but the PR removes the Inno Setup language file installation step and changes the packaging format to ZIP. This creates an inconsistency: the workflow still tries to run Inno Setup and expects an .exe installer to be created, but the main packaging approach has shifted to ZIP files. Either remove the Inno Setup step entirely or clarify the intent to support both packaging formats.
      - name: Inno Setup 打包
        if: matrix.platform == 'windows'
        run: |
          echo "开始 Inno Setup 打包..."
          
          # 确保构建输出目录存在
          if (!(Test-Path "build")) {
            mkdir build
          }
          
          # 运行 Inno Setup 编译器
          & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" SRsetup.iss
          
          # 检查安装程序是否生成并移动到 zip 目录
          if (Test-Path "build/SecRandom setup x64.exe") {
            # 修改文件名以包含版本号,方便识别
            $setupName = "SecRandom-setup-${{ github.ref_name }}-${{ matrix.arch }}.exe"
            Move-Item "build/SecRandom setup x64.exe" "zip/$setupName"
            echo "Inno Setup 打包完成: zip/$setupName"
          } else {
            echo "错误:Inno Setup 安装程序未生成"
            exit 1
          }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +219 to +234
for file in file_list:
# 构建目标文件路径
target_file = Path(target_dir) / file

# Windows 系统
if os.name == "nt":
logger.info("Windows 系统,启动安装程序")
# 确保父目录存在
ensure_dir(target_file.parent)

# 使用 start 命令启动安装程序,不等待安装完成
# 使用 DETACHED_PROCESS 标志创建新进程
DETACHED_PROCESS = 0x00000008
subprocess.Popen(
[exe_path],
creationflags=DETACHED_PROCESS,
close_fds=True,
)
else:
logger.warning("非 Windows 系统,不支持运行 EXE 安装程序")
return False
# 如果文件已存在且不允许覆盖,跳过
if target_file.exists() and not overwrite:
logger.debug(f"文件已存在,跳过: {target_file}")
continue

logger.info("安装程序已启动,准备退出应用程序")
# 解压文件
zip_ref.extract(file, target_dir)
# 添加到已解压文件列表
extracted_files.add(file)
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extract_zip function doesn't properly handle directory entries in ZIP files. When iterating through zip_ref.namelist(), directory entries end with '/' and attempting to extract them as files may cause issues. You should check if the entry is a directory using file.endswith('/') or by checking if it's a directory entry before trying to extract it. Additionally, ensure_dir on line 224 is called for target_file.parent, but if the file itself is a directory entry, this logic might not work correctly.

Copilot uses AI. Check for mistakes.
except Exception as e:
logger.exception(f"更新安装脚本执行失败: {e}")
sys.exit(1)
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The embedded installer script doesn't properly clean up the temporary script file after execution. The script runs in a subprocess and exits, but there's no mechanism to delete the temporary Python script file that was created. This will leave orphaned script files in the temp directory after each update. Consider adding cleanup logic at the end of the embedded script's main block.

Suggested change
sys.exit(1)
sys.exit(1)
finally:
# 在子进程内部自删除临时安装脚本文件
try:
import os
script_path = os.path.abspath(__file__)
logger.info(f"准备删除安装脚本文件: {script_path}")
os.remove(script_path)
logger.info(f"安装脚本文件已删除: {script_path}")
except Exception as cleanup_error:
logger.exception(f"删除安装脚本文件失败: {cleanup_error}")

Copilot uses AI. Check for mistakes.
Comment on lines +1121 to +1130
# Linux系统下设置可执行权限
if os.name != 'nt' and file.endswith(('.py', '.sh')):
try:
# 获取文件的当前权限
current_mode = os.stat(target_file).st_mode
# 添加执行权限
os.chmod(target_file, current_mode | 0o111)
logger.info(f"已设置文件执行权限: {target_file}")
except Exception as e:
logger.warning(f"设置文件执行权限失败: {e}")
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The embedded installer script attempts to set executable permissions for files ending with '.py' or '.sh' on Linux systems. However, after extraction, it only checks file extensions without verifying if the target is actually a file (not a directory). If a directory name ends with '.py' or '.sh', this could fail. Add a check to ensure target_file.is_file() before attempting to chmod.

Copilot uses AI. Check for mistakes.
Comment on lines +977 to +979
temp_script_path = (
get_path("TEMP") / "installer_temp_script.py"
) # 初始化为临时脚本路径,便于后续清理
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The temp_script_path variable is initialized at line 977-979 to get_path("TEMP") / "installer_temp_script.py", but this value is immediately overwritten at line 1243 when the NamedTemporaryFile is created. The initial assignment serves no purpose since NamedTemporaryFile creates its own unique filename. This initialization is misleading and should be removed or the variable should be initialized to None.

Suggested change
temp_script_path = (
get_path("TEMP") / "installer_temp_script.py"
) # 初始化为临时脚本路径,便于后续清理
temp_script_path = None # 初始化为 None,待实际创建临时脚本路径后再赋值

Copilot uses AI. Check for mistakes.
Comment on lines +1084 to +1136
def ensure_dir(path):
# 确保目录存在
Path(path).mkdir(parents=True, exist_ok=True)
def extract_zip(zip_path, target_dir, overwrite=True):
# 解压zip文件
try:
logger.info(f"开始解压文件: {zip_path} 到 {target_dir}")
ensure_dir(target_dir)
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
for file in zip_ref.namelist():
target_file = Path(target_dir) / file
ensure_dir(target_file.parent)
if target_file.exists() and not overwrite:
logger.info(f"文件已存在,跳过: {target_file}")
continue
# 解压文件
try:
zip_ref.extract(file, target_dir)
logger.info(f"解压文件成功: {target_file}")
except PermissionError:
# Windows 上可能需要删除旧文件
if target_file.exists():
try:
target_file.unlink()
zip_ref.extract(file, target_dir)
logger.info(f"重新解压文件成功: {target_file}")
except Exception as e:
logger.warning(f"覆盖文件失败,跳过: {target_file}, 错误: {e}")
continue
else:
raise
# Linux系统下设置可执行权限
if os.name != 'nt' and file.endswith(('.py', '.sh')):
try:
# 获取文件的当前权限
current_mode = os.stat(target_file).st_mode
# 添加执行权限
os.chmod(target_file, current_mode | 0o111)
logger.info(f"已设置文件执行权限: {target_file}")
except Exception as e:
logger.warning(f"设置文件执行权限失败: {e}")
logger.info(f"文件解压完成: {zip_path} 到 {target_dir}")
return True
except Exception as e:
logger.exception(f"解压文件失败: {e}")
return False
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The embedded installer script reimplements functions like ensure_dir and extract_zip that already exist in the parent module. This code duplication increases maintenance burden. If bugs are fixed in the main functions, they won't automatically be fixed in the embedded script. Consider extracting these utility functions to a separate module that can be imported by the installer script, or use a different approach that doesn't require embedding a complete installer script.

Copilot uses AI. Check for mistakes.
Comment on lines +1252 to +1259
subprocess.Popen(
[sys.executable, temp_script_path, file_path, str(root_dir)],
close_fds=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NO_WINDOW
if sys.platform == "win32"
else 0,
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subprocess.CREATE_NO_WINDOW constant is only available on Windows platforms. While the code uses a conditional expression to check sys.platform == "win32", if this code runs on a non-Windows platform, the name subprocess.CREATE_NO_WINDOW will still be evaluated and will raise an AttributeError. The constant should be accessed conditionally or imported with a try-except block.

Suggested change
subprocess.Popen(
[sys.executable, temp_script_path, file_path, str(root_dir)],
close_fds=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NO_WINDOW
if sys.platform == "win32"
else 0,
creationflags = 0
if sys.platform == "win32":
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
subprocess.Popen(
[sys.executable, temp_script_path, file_path, str(root_dir)],
close_fds=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=creationflags,

Copilot uses AI. Check for mistakes.
Comment on lines +1144 to +1151
main_program = None
possible_main_files = ['main.py', 'SecRandom', 'SecRandom.exe']
for main_file in possible_main_files:
main_path = Path(root_dir) / main_file
if main_path.exists():
main_program = main_path
break
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The embedded installer script has a potential issue where it checks for multiple possible main program files (main.py, SecRandom, SecRandom.exe) but doesn't handle the case where multiple files might exist. The logic uses the first match found, which could lead to incorrect program restart if multiple candidates exist in the directory. Consider adding logic to prioritize the correct executable based on the platform.

Copilot uses AI. Check for mistakes.
Comment on lines +1161 to +1166
# Windows系统 - 使用引号保护路径中的空格
logger.info("Windows系统,使用start命令重启")
import subprocess
subprocess.Popen(
['start', '""', f'"{main_program}"'],
shell=True,
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows restart command uses an unusual syntax with nested quotes: ['start', '""', f'"{main_program}"']. The empty string '""' after 'start' is meant to be the window title parameter, but this syntax may not work correctly with subprocess.Popen when shell=True. Consider using a more standard approach such as directly launching the executable without the 'start' command or using the proper syntax for the start command.

Suggested change
# Windows系统 - 使用引号保护路径中的空格
logger.info("Windows系统,使用start命令重启")
import subprocess
subprocess.Popen(
['start', '""', f'"{main_program}"'],
shell=True,
# Windows系统 - 直接启动主程序
logger.info("Windows系统,直接启动主程序重启")
subprocess.Popen(
[str(main_program)],

Copilot uses AI. Check for mistakes.
if __name__ == '__main__':
try:
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The embedded installer script's main block doesn't validate that exactly 2 arguments are provided. If the script is called with fewer arguments, sys.argv[1] or sys.argv[2] will raise an IndexError. Add validation to check len(sys.argv) before accessing the arguments and provide a helpful error message if the wrong number of arguments is provided.

Suggested change
try:
try:
# 验证参数数量
if len(sys.argv) != 3:
logger.error("参数数量错误。用法: python update_utils.py <update_file> <root_dir>")
sys.exit(1)

Copilot uses AI. Check for mistakes.
Comment on lines +1020 to +1022
shutil.rmtree(old_version_dir)
logger.info(f"删除旧版本目录: {old_version_dir}")

Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The development environment update logic deletes the old version directory (old_version_dir) after extracting to TEMP, but this could cause issues if the current process is running from that directory. Deleting the directory the application is running from could cause file access issues or crashes. Consider whether this deletion is necessary in development mode, or ensure it happens after the application has fully exited.

Suggested change
shutil.rmtree(old_version_dir)
logger.info(f"删除旧版本目录: {old_version_dir}")
try:
# 防止删除当前正在运行的目录
current_dir = Path(__file__).resolve().parent
cwd = Path.cwd().resolve()
old_version_dir_resolved = old_version_dir.resolve()
def _is_within(child: "Path", parent: "Path") -> bool:
try:
child.relative_to(parent)
return True
except Exception:
return False
# 如果当前脚本所在目录或当前工作目录在旧版本目录内,则不删除
if (
old_version_dir_resolved == current_dir
or old_version_dir_resolved == cwd
or _is_within(current_dir, old_version_dir_resolved)
or _is_within(cwd, old_version_dir_resolved)
):
logger.warning(
f"检测到当前进程正在从旧版本目录运行,跳过删除操作: {old_version_dir_resolved}"
)
else:
shutil.rmtree(old_version_dir_resolved)
logger.info(f"删除旧版本目录: {old_version_dir_resolved}")
except Exception as e:
logger.exception(f"删除旧版本目录失败: {e}")

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant