Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Build (new Nuitka)

on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
version:
description: "版本号 (e.g. v2.3.10)"
required: true
type: string

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
platform: windows
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v5
with:
python-version: "3.13.5"

- name: Determine version
id: version
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
V="${{ github.event.inputs.version }}"
else
V="${{ github.ref_name }}"
fi
PLAIN="${V#v}"
echo "version=$V" >> $GITHUB_OUTPUT
echo "plain=$PLAIN" >> $GITHUB_OUTPUT
echo "Version: $V"

- name: Update version in source
run: uv run python update_version.py
env:
VERSION: ${{ steps.version.outputs.version }}

- name: Build with Nuitka
run: uv run python build_nuitka.py
env:
CI: "true"

- name: Copy language modules
shell: pwsh
run: |
$dst = "dist/main.dist/app/Language/modules"
# -Force 会自动递归创建不存在的父目录
New-Item -ItemType Directory -Force -Path $dst | Out-Null
# 确保源文件存在再复制,避免报错
if (Test-Path "app/Language/modules/*.py") {
Copy-Item -Path "app/Language/modules/*.py" -Destination $dst -Force
} else {
Write-Warning "Source language modules not found!"
}

- name: Pack artifact
shell: pwsh
run: |
$src = "dist/main.dist"
$folderName = "SecRandom-${{ steps.version.outputs.plain }}-${{ matrix.platform }}-x64"
$dst = "dist/$folderName"

# 安全重命名:如果目标已存在,先删除
if (Test-Path $dst) { Remove-Item -Recurse -Force $dst }
Rename-Item -Path $src -NewName $folderName

# 更改压缩路径:压缩整个文件夹,这样解压后包含外层目录,不会散落一地
Compress-Archive -Path $dst -DestinationPath "$dst.zip" -Force

echo "artifact=$dst" | Out-File -Append -FilePath $env:GITHUB_OUTPUT -Encoding utf8
echo "zip=$dst.zip" | Out-File -Append -FilePath $env:GITHUB_OUTPUT -Encoding utf8

- uses: actions/upload-artifact@v4
with:
name: SecRandom-${{ steps.version.outputs.plain }}-${{ matrix.platform }}-x64
path: dist/SecRandom-${{ steps.version.outputs.plain }}-${{ matrix.platform }}-x64.zip

- name: Create release
# 允许通过 Tag 触发,或者手动触发时也创建 Release 草稿
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
uses: softprops/action-gh-release@v2
with:
name: ${{ steps.version.outputs.version }}
tag_name: ${{ steps.version.outputs.version }} # 手动触发时明确指定 Tag 名
draft: true
files: |
dist/SecRandom-${{ steps.version.outputs.plain }}-${{ matrix.platform }}-x64.zip
131 changes: 26 additions & 105 deletions build_nuitka.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,102 +3,52 @@
用于构建 SecRandom 的独立可执行文件
"""

import os
import subprocess
import sys
import re
from pathlib import Path

# 设置Windows控制台编码为UTF-8
if sys.platform == "win32":
import io

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")

from packaging_utils import (
ADDITIONAL_HIDDEN_IMPORTS,
ICON_FILE,
PROJECT_ROOT,
collect_data_includes,
collect_language_modules,
collect_view_modules,
normalize_hidden_imports,
)

# 导入项目配置信息
sys.path.insert(0, str(Path(__file__).parent))
from app.tools.variable import APPLY_NAME, VERSION, APP_DESCRIPTION, AUTHOR, WEBSITE

# 导入deb包构建工具
from packaging_utils_deb import DebBuilder

PACKAGE_INCLUDE_NAMES = {
"app.Language.modules",
"app.view",
"app.tools",
"app.page_building",
}


def _print_packaging_summary() -> None:
"""Log a quick overview of the data and modules that will be bundled."""

data_includes = collect_data_includes()
hidden_names = normalize_hidden_imports(
collect_language_modules() + collect_view_modules() + ADDITIONAL_HIDDEN_IMPORTS
)
package_names = sorted(
{name for name in hidden_names if "." not in name} | PACKAGE_INCLUDE_NAMES
)
module_names = [name for name in hidden_names if "." in name]

print("\nSelected data includes ({} entries):".format(len(data_includes)))
print("\n数据文件 ({} entries):".format(len(data_includes)))
for item in data_includes:
kind = "dir " if item.is_dir else "file"
print(f" - {kind} {item.source} -> {item.target}")

print("\nRequired packages ({} entries):".format(len(package_names)))
for pkg in package_names:
print(f" - {pkg}")

print("\nHidden modules ({} entries):".format(len(module_names)))
for mod in module_names:
print(f" - {mod}")
kind = "dir" if item.is_dir else "file"
print(f" {kind} {item.source} -> {item.target}")
print("\n动态导入包: --include-package=app (递归包含所有子包)")


def _gather_data_flags() -> list[str]:
"""收集数据文件包含标志"""
flags: list[str] = []
for include in collect_data_includes():
flag = "--include-data-dir" if include.is_dir else "--include-data-file"
source = include.source
target = include.target
# FIX: Nuitka 不允许 file 目标为 "."
if not include.is_dir and target == ".":
target = Path(source).name
flags.append(f"{flag}={source}={target}")
return flags


def _gather_module_and_package_flags() -> tuple[list[str], list[str]]:
"""收集模块和包包含标志"""
hidden_names = normalize_hidden_imports(
collect_language_modules() + collect_view_modules() + ADDITIONAL_HIDDEN_IMPORTS
)
package_names = set(PACKAGE_INCLUDE_NAMES)
module_names: list[str] = []
for name in hidden_names:
if "." not in name:
package_names.add(name)
else:
module_names.append(name)
package_flags = [f"--include-package={pkg}" for pkg in sorted(package_names)]
module_flags = [f"--include-module={mod}" for mod in module_names]
return module_flags, package_flags


def _sanitize_version(ver_str: str) -> str:
"""清理版本字符串,确保符合Nuitka要求"""
if not ver_str:
return "0.0.0.0"
ver_str = ver_str.lstrip("vV").strip()
Expand All @@ -112,20 +62,15 @@ def _sanitize_version(ver_str: str) -> str:


def get_nuitka_command() -> list[str]:
"""获取Nuitka命令列表"""
raw_version = VERSION if VERSION else "0.0.0"
clean_version = _sanitize_version(raw_version)
print(f"\n版本号处理: '{raw_version}' -> '{clean_version}'")

module_flags, package_flags = _gather_module_and_package_flags()
print(f"\n版本号: '{raw_version}' -> '{clean_version}'")

cmd = [
"uv",
"run",
sys.executable,
"-m",
"nuitka",
"--standalone",
"--onefile",
"--enable-plugin=pyside6",
"--assume-yes-for-downloads",
"--output-dir=dist",
Expand All @@ -137,25 +82,20 @@ def get_nuitka_command() -> list[str]:
"--no-deployment-flag=self-execution",
]

# 编译器选择逻辑
if sys.platform == "win32":
# 检测是否为 Python 3.13 及以上
if sys.version_info >= (3, 13):
print("\n[注意] 检测到 Python 3.13+")
print(" Nuitka 暂不支持在此版本使用 MinGW64。")
print(
" 将自动切换为 MSVC (Visual Studio)。请确保已安装 C++ 生成工具。"
)
print("\n[注意] Python 3.13+ 不支持 MinGW64,将使用 MSVC。")
print(" 请确保已安装 Visual Studio C++ 生成工具。")
cmd.append("--msvc=latest")
else:
# Python 3.12 及以下使用 MinGW64
cmd.append("--mingw64")
else:
cmd.append("--linux-onefile-icon")

cmd.extend(_gather_data_flags())
cmd.extend(package_flags)
cmd.extend(module_flags)

# 递归包含 app/ 下所有子包(覆盖语言模块、设置页面等动态导入)
cmd.append("--include-package=app")

if sys.platform == "win32" and ICON_FILE.exists():
cmd.append(f"--windows-icon-from-ico={ICON_FILE}")
Expand All @@ -167,15 +107,12 @@ def get_nuitka_command() -> list[str]:


def check_compiler_env() -> bool:
"""检查编译器环境"""
if sys.platform != "win32":
return True

# 如果是 Python 3.13+,需要检查 MSVC(这里简单略过,交给 Nuitka 报错,因为检测 MSVC 比较复杂)
if sys.version_info >= (3, 13):
return True

# 如果是 Python < 3.13,检查 MinGW64
print("\n检查 MinGW64 环境...")
try:
result = subprocess.run(
Expand All @@ -187,66 +124,59 @@ def check_compiler_env() -> bool:
errors="replace",
)
if result.returncode == 0:
print(
f"✓ 找到 GCC: {result.stdout.splitlines()[0] if result.stdout else 'Unknown'}"
)
line = result.stdout.splitlines()[0] if result.stdout else "Unknown"
print(f"找到 GCC: {line}")
return True
except FileNotFoundError:
pass

# 简单检查路径
common_paths = [
r"C:\msys64\mingw64\bin",
r"C:\mingw64\bin",
r"C:\Program Files\mingw64\bin",
]
for path in common_paths:
if (Path(path) / "gcc.exe").exists():
print(f"找到 MinGW64: {path}")
for p in common_paths:
if (Path(p) / "gcc.exe").exists():
print(f"找到 MinGW64: {p}")
return True

print("⚠ 警告: 未找到 MinGW64,Nuitka 可能会尝试自动下载。")
print("未找到 MinGW64,Nuitka 将自动下载。")
return input("是否继续? (y/n): ").lower() == "y"


def build_deb() -> None:
"""构建deb包"""
if sys.platform != "linux":
return

print("\n" + "=" * 60)
print("开始构建deb包...")
print("开始构建 deb 包...")
print("=" * 60)

try:
DebBuilder.build_from_nuitka(
PROJECT_ROOT, APPLY_NAME, VERSION, APP_DESCRIPTION, AUTHOR, WEBSITE
)
print("=" * 60)

except Exception as e:
print(f"构建deb包失败: {e}")
print(f"构建 deb 包失败: {e}")
sys.exit(1)


def main():
"""执行 Nuitka 打包"""
print("=" * 60)
print("开始使用 Nuitka + uv 打包 SecRandom")
print("Nuitka 打包 SecRandom")
print("=" * 60)

if sys.platform == "win32" and not check_compiler_env():
if not os.environ.get("CI") and sys.platform == "win32" and not check_compiler_env():
sys.exit(1)

_print_packaging_summary()
cmd = get_nuitka_command()

# 打印命令
print("\n执行命令:")
print(" ".join(cmd))
print("\n" + "=" * 60)

# 执行打包
try:
subprocess.run(
cmd,
Expand All @@ -258,30 +188,21 @@ def main():
errors="replace",
)
print("\n" + "=" * 60)
print("Nuitka打包成功!")
print("Nuitka 打包成功!")
print("=" * 60)

# 构建deb包(仅在Linux平台)
build_deb()

except subprocess.CalledProcessError as e:
print("\n" + "=" * 60)
print(f"打包失败: {e}")
print(f"返回码: {e.returncode}")
print("=" * 60)
print(f"\n打包失败 (返回码 {e.returncode})")
sys.exit(1)
except KeyboardInterrupt:
print("\n" + "=" * 60)
print("用户取消打包")
print("=" * 60)
print("\n用户取消")
sys.exit(1)
except Exception as e:
print("\n" + "=" * 60)
print(f"发生意外错误: {e}")
print(f"\n错误: {e}")
import traceback

traceback.print_exc()
print("=" * 60)
sys.exit(1)


Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ build-backend = "setuptools.build_meta"
py-modules = []

[[tool.uv.index]]
url = "https://mirrors.aliyun.com/pypi/simple"
url = "https://mirrors.aliyun.com/pypi/simple/"
default = true

[dependency-groups]
Expand Down
Loading
Loading