first commit

This commit is contained in:
2026-06-25 17:41:06 +08:00
commit b2f23a933b
370 changed files with 30526 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
python -m nuitka \
--standalone \
--onefile \
--follow-imports \
--lto=yes \
--jobs=8 \
--remove-output \
--output-dir=dist \
--enable-plugin=numpy \
--include-package=pymysql \
--include-module=sqlalchemy.dialects.mysql \
--include-module=sqlalchemy.engine \
--include-module=sqlalchemy.sql \
--include-module=cv2 \
--noinclude-custom-mode=cv2.gui:warning \
--nofollow-import-to=matplotlib,scipy,pandas,tkinter,test \
--noinclude-default-mode=error \
--python-flag=no_docstrings \
--noinclude-data-files=*.png,*.mp4,*.xls* \
main.py
+367
View File
@@ -0,0 +1,367 @@
import configparser
import subprocess
import shutil
import argparse
import requests
from pathlib import Path
import time
import os
import sys
sys.path.append(str(Path(__file__).resolve().parent.parent))
from build import get_settings
def download_file(url, dest):
"""下载文件到指定路径"""
try:
r = requests.get(url, stream=True)
r.raise_for_status()
with open(dest, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return True
except Exception as e:
print(f"下载失败: {e}")
return False
def load_config(config_path):
"""加载配置文件"""
return get_settings()
config = configparser.ConfigParser()
try:
# 指定 UTF-8 编码读取配置文件
config.read(config_path, encoding='utf-8')
if not config.sections(): # 检查是否成功读取到配置项
raise ValueError(f"配置文件 {config_path} 为空或格式错误")
return config
except FileNotFoundError:
raise FileNotFoundError(f"配置文件 {config_path} 不存在")
except UnicodeDecodeError:
# 尝试其他常见编码
try:
config.read(config_path, encoding='gbk')
if not config.sections():
raise ValueError(f"配置文件 {config_path} 为空或格式错误")
return config
except Exception as e:
raise ValueError(f"配置文件 {config_path} 编码错误: {str(e)}")
except Exception as e:
raise ValueError(f"加载配置文件 {config_path} 失败: {str(e)}")
def build_nuitka_command(config, args, app_name):
"""构造Nuitka编译命令"""
cmd = []
# 基本选项
if compiler := args.compiler or config.nuitka.compiler:
cmd.append(f"--{compiler}")
if args.show_progress or config.nuitka.show_progress:
cmd.append("--show-progress")
if args.standalone or config.nuitka.standalone:
cmd.append("--standalone")
if args.onefile or config.nuitka.onefile:
cmd.append("--onefile")
if config.nuitka.remove_output:
cmd.append("--remove-output")
if args.disable_console or config.nuitka.disable_console:
cmd.append("--windows-disable-console")
# 版本信息
if product_version := config.version.version:
cmd.append(f"--product-version={product_version}")
if file_version := config.version.version_str:
cmd.append(f"--file-version={file_version}")
if company_name := config.version.author:
cmd.append(f"--company-name={company_name}")
if product_name := (config.version.alias or config.version.name):
cmd.append(f"--product-name={product_name}")
cmd.append(f"--output-filename={app_name}")
# 添加新的编译选项
if config.nuitka.get('follow_imports', True):
cmd.append("--follow-imports")
if lto := config.nuitka.lto:
cmd.append(f"--lto={lto}")
if jobs := args.jobs or config.nuitka.jobs:
cmd.append(f"--jobs={jobs}")
# 插件处理
if plugins := config.nuitka.enable_plugin:
for plugin in plugins.split(','):
cmd.append(f"--enable-plugin={plugin.strip()}")
# 包和模块包含
if packages := config.nuitka.include_package:
for package in packages.split(','):
cmd.append(f"--include-package={package.strip()}")
if modules := config.nuitka.include_module:
for module in modules.split(','):
cmd.append(f"--include-module={module.strip()}")
# 不跟随的导入
if nofollow := config.nuitka.nofollow_import_to:
for item in nofollow.split(','):
cmd.append(f"--nofollow-import-to={item.strip()}")
# 数据文件排除
if noinclude_data := config.nuitka.noinclude_data_files:
for pattern in noinclude_data.split(','):
cmd.append(f"--noinclude-data-files={pattern.strip()}")
# Python标志
if python_flags := config.nuitka.python_flag:
for flag in python_flags.split(','):
cmd.append(f"--python-flag={flag.strip()}")
# 图标处理
icon_source = args.icon or config.build.icon_file
if icon_source:
ico_path = 'favicon.ico'
if 'ico' not in icon_source:
from tranform_to_ico import convert_png_to_ico
convert_png_to_ico(icon_source, ico_path, sizes=[(256, 256)], sharpen=False)
else:
ico_path = icon_source
if ico_path.startswith(('http://', 'https://')):
local_icon = Path("download.ico")
if download_file(ico_path, local_icon):
cmd.append(f"--windows-icon-from-ico={local_icon}")
else:
cmd.append(f"--windows-icon-from-ico={ico_path}")
# 额外选项
if extra_options := config.nuitka.extra_options:
if isinstance(extra_options, str):
extra_options = extra_options.split(',')
cmd.extend(extra_options)
# 输出目录
output_dir = args.output or config.build.output_dir or 'dist'
cmd.append(f"--output-dir={output_dir}")
python_exe = sys.executable
cmd = [python_exe, "-m", "nuitka"] + list(set(cmd))
# 源文件
source_script = args.script or config.build.source_script or 'source_script'
cmd.append(source_script)
return cmd
def sign_executable(config, args):
"""签名可执行文件"""
if not args.sign:
print("跳过签名步骤")
return True
sign_tool = config.signing.sign_tool
if not sign_tool or not Path(sign_tool).exists():
print("警告: signtool.exe 未找到,跳过签名")
return False
cert_file = args.cert or config.signing.cert_file
if not cert_file or not Path(cert_file).exists():
print("警告: 证书文件不存在,跳过签名")
return False
cert_password = args.password or config.signing.cert_password or ''
timestamp_server = args.timestamp or config.signing.timestamp_server or 'http://timestamp.digicert.com'
output_dir = args.output or config.build.output_dirt
source_script = args.script or config.build.source_script
exe_name = Path(source_script).stem + ".exe"
exe_path = Path(output_dir) / exe_name
sign_cmd = [
sign_tool,
"sign",
"/tr", timestamp_server,
"/td", "sha256",
"/fd", "sha256",
"/f", cert_file,
"/p", cert_password,
str(exe_path),
]
try:
subprocess.run(sign_cmd, check=True)
print(f"✓ 签名成功: {exe_path}")
# 验证签名
verify_cmd = [sign_tool, "verify", "/pa", str(exe_path)]
subprocess.run(verify_cmd, check=True)
return True
except subprocess.CalledProcessError as e:
print(f"✗ 签名失败: {e}")
return False
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="Python打包签名工具")
parser.add_argument('--config', default='build_config.ini',
help="配置文件路径 (默认: build_config.ini)")
parser.add_argument('--script', help="覆盖配置中的源脚本")
parser.add_argument('--output', help="覆盖输出目录")
parser.add_argument('--icon', help="本地或在线ICO图标路径 (http/https)")
parser.add_argument('--jobs', help="并行进程数")
parser.add_argument('--compiler', default="mingw64", help="编译器类型")
parser.add_argument('--show-progress', default="store_true", help="显示编译进度")
parser.add_argument('--standalone', action='store_true', help="强制启用standalone模式")
parser.add_argument('--onefile', action='store_true', help="强制启用单文件模式")
parser.add_argument('--disable-console', action='store_true',
help="禁用控制台窗口 (GUI程序)")
parser.add_argument('--no-sign', dest='sign', action='store_false',
help="跳过签名步骤")
parser.add_argument('--cert', help="覆盖证书路径")
parser.add_argument('--password', help="证书密码")
parser.add_argument('--timestamp', help="覆盖时间戳服务器URL")
return parser.parse_args()
def format_file_size(size_bytes):
"""将文件大小转换为易读格式"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024.0:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.2f} TB"
def get_exe_path(config, args):
"""获取生成的exe文件路径"""
output_dir = args.output or config.build.output_dirt or 'dist'
source_script = args.script or config.build.source_script
exe_name = Path(source_script).stem + (".exe" if os.name == 'nt' else "")
return Path(output_dir) / exe_name
def main():
args = parse_args()
print("=== 开始构建 ===")
start_time = time.time()
build_success = True
sign_success = False
try:
config = load_config(args.config)
except FileNotFoundError as e:
print(f"错误: {e}")
return
# import json; print(json.dumps(config, indent=4, ensure_ascii=False))
# import json; print(json.dumps(get_settings(), indent=4, ensure_ascii=False))
# return
# 清理旧构建
output_dir = args.output or config.build.output_dir or 'dist'
if config.build.clean and Path(output_dir).exists():
try:
shutil.rmtree(output_dir)
Path(output_dir).mkdir(parents=True, exist_ok=True)
print(f"已清理输出目录: {output_dir}")
except Exception as e:
print(f"清理目录时出错: {e}")
build_success = False
product_name = (config.version.alias or config.version.name)
product_version = config.version.version
app_name = f"{product_name}{'v'+product_version if product_version else ''}"
print(f"应用名称: {app_name}")
build_success = True
if build_success:
# 编译
print("\n[1/3] 正在使用Nuitka编译...")
try:
nuitka_cmd = build_nuitka_command(config, args, app_name)
print("执行命令:", " ".join(nuitka_cmd))
compile_start = time.time()
subprocess.run(nuitka_cmd, check=True)
print(f"编译完成,耗时: {time.time() - compile_start:.2f}")
except subprocess.CalledProcessError as e:
print(f"编译失败: {e}")
build_success = False
if build_success and args.sign:
# 签名
print("\n[2/3] 正在处理签名...")
try:
sign_start = time.time()
sign_success = sign_executable(config, args)
if sign_success:
print(f"签名完成,耗时: {time.time() - sign_start:.2f}")
except Exception as e:
print(f"签名过程中出错: {e}")
sign_success = False
if build_success:
# 压缩文件
print("\n[3/3] 正在压缩文件...")
import zipfile
from datetime import datetime
zip_path = f"{output_dir}/{app_name}.zip"
try:
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for file in [f'{output_dir}/{app_name}.exe', *(config.build.resource_files or [])]:
if not file: continue
filename = Path(file).name
zipf.write(file, arcname=filename)
print(f"压缩完成: {zip_path}")
except Exception as e:
print(f"压缩文件时出错: {e}")
build_success = False
zip_app_name = f"{config.version.name}{'v'+product_version if product_version else ''}.zip"
version_data = config.version.to_dict()
save_url = config.build.save_url.strip('/')
access_url = config.version.access_url.strip('/')
version_data['url'] = access_url + '/' + zip_app_name
import json
with open(f'{output_dir}/version.json', 'w', encoding='utf-8') as f:
json.dump(dict(
data=version_data,
code=200,
success='true',
message=f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - {app_name} 打包完成',
), f, ensure_ascii=False, indent=4)
print(f"版本信息已保存到: {output_dir}/version.json")
if not os.system(f'curl -T {zip_path} {save_url}/{zip_app_name}'):
print(f"上传完成: {version_data['url']}")
os.system(f'curl -T {output_dir}/version.json {save_url}/latest')
# 构建总结
print("\n=== 构建总结 ===")
total_time = time.time() - start_time
status = "成功" if build_success else "失败"
summary = [
f"构建状态: {status}",
f"总耗时: {total_time:.2f}",
f"输出目录: {output_dir}",
]
if build_success:
exe_path = get_exe_path(config, args)
if exe_path.exists():
file_time = time.strftime('%Y-%m-%d %H:%M:%S',
time.localtime(exe_path.stat().st_mtime))
summary.extend([
f"生成文件: {exe_path}",
f"文件大小: {format_file_size(exe_path.stat().st_size)}",
f"修改时间: {file_time}",
])
if args.sign:
summary.append(f"签名状态: {'成功' if sign_success else '失败'}")
print("\n".join(summary))
if not build_success:
sys.exit(1)
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
python scripts/generate_pyi.py image_loader.py
+125
View File
@@ -0,0 +1,125 @@
# generate_pyi.py
import re
import os
import argparse
from pathlib import Path
def sanitize_name(name: str) -> str:
name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
return re.sub(r'^[^a-zA-Z_]+', '', name)
def pascal_case(s: str) -> str:
"""将字符串转为 PascalCase"""
return ''.join(word.capitalize() for word in re.split(r'[\W_]+', s) if word)
def generate_pyi(base_dir: str, output_file: str):
base_path = Path(base_dir)
if not base_path.exists():
raise FileNotFoundError(f"资源目录不存在: {base_dir}")
# 构建目录树结构
tree = {}
for root, dirs, files in os.walk(str(base_path)):
relative_path = Path(root).relative_to(base_path)
parts = list(relative_path.parts) if relative_path != Path('.') else []
# 插入当前路径到树中
current = tree
for part in parts:
safe_part = sanitize_name(part)
if safe_part not in current:
current[safe_part] = {}
current = current[safe_part]
# 添加图像文件作为叶子节点
for file in files:
if file.startswith('.'):
continue
if file.split('.')[-1].lower() in {'png', 'jpg', 'jpeg', 'bmp', 'gif'}:
stem = sanitize_name(Path(file).stem)
current[stem] = None # 表示这是一个图像资源(str 类型)
# 构建类定义
class_defs = []
written_classes = set()
def build_class(class_name: str, data: dict):
if not data or class_name in written_classes:
return
written_classes.add(class_name)
lines = [f"class {class_name}:\n"]
has_content = False
for key in sorted(data.keys()):
value = data[key]
safe_key = key
if value is None:
# 图像文件:直接定义为 str 属性
lines.append(f" {safe_key}: str\n")
has_content = True
else:
# 子目录:引用一个类
nested_class_name = pascal_case(safe_key)
lines.append(f" {safe_key}: \"{nested_class_name}\"\n")
build_class(nested_class_name, value)
has_content = True
if has_content:
class_defs.append("".join(lines))
else:
# 空类不写入
written_classes.remove(class_name)
# 构建主类 ImageResourceLoader
loader_lines = []
for key in sorted(tree.keys()):
value = tree[key]
if not value: # 跳过空的子目录
loader_lines.append(f" {key}: str\n")
continue
class_name = pascal_case(key)
loader_lines.append(f" {key}: {class_name}\n")
build_class(class_name, value)
# 写入 .pyi 文件
with open(output_file, 'w', encoding='utf-8') as f:
f.write("from typing import Optional, Any\n\n")
# 写入所有子类定义
for line in class_defs:
f.write(line + "\n")
# 写入主类
f.write("class ImageResourceLoader:\n")
f.write(" def __init__(self, base_dir: str): ...\n")
f.write(" def list_resources(self) -> None: ...\n")
f.write(" def get(self, path: str, default: Optional[str] = ...) -> Optional[str]: ...\n")
f.write(" def __getattr__(self, name: str) -> Any: ...\n")
f.write("\n # 自动生成的资源类属性(仅用于 IDE 提示)\n")
f.writelines(loader_lines)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="为 ImageResourceLoader 类生成 .pyi 存根文件。")
parser.add_argument("source_file", help="Python 源文件路径(例如:image_loader.py")
parser.add_argument("--base_dir", default=".automation",
help="图像资源目录,默认值:.automation")
args = parser.parse_args()
source_path = Path(args.source_file)
if not source_path.exists():
raise FileNotFoundError(f"源文件不存在: {args.source_file}")
# 输出 .pyi 文件路径
pyi_path = source_path.with_suffix(".pyi")
print(f"生成存根文件: {pyi_path}")
print(f"使用资源目录: {args.base_dir}")
generate_pyi(args.base_dir, str(pyi_path))
"""
python scripts/generate_pyi.py image_loader.py --base_dir assets/images
"""
+15
View File
@@ -0,0 +1,15 @@
source scripts/venv.sh
cd app/utils/some_sdk
git pull origin main
cd ../../../
export DB_CONNECTION=mysql
export DB_HOST=127.0.0.1
export DB_PORT=3306
export DB_NAME=my_db
export DB_USER=root
export DB_PASSWORD=xxxxxx
python run.py
+74
View File
@@ -0,0 +1,74 @@
from PIL import Image, ImageEnhance, ImageFilter
def convert_png_to_ico(png_path, ico_path, sizes=None, resample=Image.LANCZOS, sharpen=False):
"""
将PNG图片转换为ICO图标文件,同时优化圆角效果
参数:
- png_path: 输入PNG文件路径
- ico_path: 输出ICO文件路径
- sizes: 图标尺寸列表
- resample: 重采样方法,默认为Image.LANCZOS
- sharpen: 是否应用锐化滤镜
"""
if sizes is None:
sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (256, 256)]
try:
with Image.open(png_path) as img:
# 确保图片为RGBA模式
if img.mode != 'RGBA':
img = img.convert('RGBA')
# 检查是否有透明通道
has_transparency = False
if img.mode == 'RGBA':
alpha = img.getchannel('A')
if alpha.getextrema()[0] < 255:
has_transparency = True
# 为每个尺寸创建单独的图像
icon_sizes = []
for size in sizes:
# 调整图片尺寸
if size[0] < img.size[0] or size[1] < img.size[1]:
# 缩小图片时使用高质量重采样算法
resized_img = img.resize(size, resample=resample)
else:
# 放大图片时使用NEAREST算法避免模糊(适用于需要清晰边缘的情况)
resized_img = img.resize(size, resample=Image.NEAREST)
# 应用锐化滤镜(可选)
if sharpen:
enhancer = ImageEnhance.Sharpness(resized_img)
resized_img = enhancer.enhance(1.5) # 增强锐度
icon_sizes.append(resized_img)
# 保存为ICO文件
if len(icon_sizes) > 0:
# 使用第一个尺寸作为主图标
icon_sizes[0].save(
ico_path,
format='ICO',
sizes=[(img.size[0], img.size[1]) for img in icon_sizes]
)
print(f"成功将 {png_path} 转换为 {ico_path}")
print(f"包含尺寸: {sizes}")
if has_transparency:
print("注意:已保留图片的透明通道(圆角效果)")
else:
print("图片没有检测到透明通道(可能没有圆角效果)")
else:
print("错误:未指定有效尺寸")
except Exception as e:
print(f"转换失败: {e}")
if __name__ == "__main__":
# 输入和输出文件路径
png_path = 'app.png'
ico_path = 'favicon.ico'
# 调用函数进行转换,禁用锐化处理
convert_png_to_ico(png_path, ico_path, sizes=[(256, 256)], sharpen=False)
+23
View File
@@ -0,0 +1,23 @@
default_value=vue-fastapi-admin
app=${1:-$default_value}
platform=$(uname -s)
if [ "${platform}" == "Linux" ]; then
env_path=/data/env/python/${app}
bin_path=${env_path}/bin
# 使用 [[ ]] 进行模式匹配
elif [[ "${platform}" == *"MINGW64"* ]]; then
env_path=/d/data/python-venv/${app}
bin_path=${env_path}/Scripts
fi
if [ -d "${env_path}" ]; then
echo "Activating virtual environment..."
source ${bin_path}/activate
else
echo "Creating virtual environment..."
python -m venv ${env_path}
echo "Activating virtual environment..."
source ${bin_path}/activate
pip install -r requirements.txt
fi