Some checks are pending
CI / Test (Python 3.10 on macos-latest) (push) Waiting to run
CI / Test (Python 3.11 on macos-latest) (push) Waiting to run
CI / Test (Python 3.12 on macos-latest) (push) Waiting to run
CI / Test (Python 3.8 on macos-latest) (push) Waiting to run
CI / Test (Python 3.9 on macos-latest) (push) Waiting to run
CI / Test (Python 3.10 on ubuntu-latest) (push) Waiting to run
CI / Test (Python 3.11 on ubuntu-latest) (push) Waiting to run
CI / Test (Python 3.12 on ubuntu-latest) (push) Waiting to run
CI / Test (Python 3.8 on ubuntu-latest) (push) Waiting to run
CI / Test (Python 3.9 on ubuntu-latest) (push) Waiting to run
CI / Test (Python 3.10 on windows-latest) (push) Waiting to run
CI / Test (Python 3.11 on windows-latest) (push) Waiting to run
CI / Test (Python 3.12 on windows-latest) (push) Waiting to run
CI / Test (Python 3.8 on windows-latest) (push) Waiting to run
CI / Test (Python 3.9 on windows-latest) (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Release (push) Blocked by required conditions
Documentation / Build Documentation (push) Waiting to run
529 lines
17 KiB
Python
529 lines
17 KiB
Python
# sikuwa/config.py
|
||
"""
|
||
Sikuwa 配置管理模块
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import List, Optional, Dict, Any, TYPE_CHECKING
|
||
from dataclasses import dataclass, field, asdict
|
||
|
||
# 修复 tomli 导入,避免 mypyc 问题
|
||
if sys.version_info >= (3, 11):
|
||
import tomllib
|
||
else:
|
||
try:
|
||
import tomli as tomllib
|
||
except ImportError:
|
||
raise ImportError(
|
||
"Python < 3.11 需要安装 tomli:\n"
|
||
" pip install tomli\n"
|
||
"或升级到 Python 3.11+"
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class NuitkaOptions:
|
||
"""Nuitka 编译选项"""
|
||
|
||
# 基础选项
|
||
standalone: bool = True
|
||
onefile: bool = False
|
||
follow_imports: bool = True
|
||
show_progress: bool = True
|
||
enable_console: bool = True
|
||
|
||
# 优化选项
|
||
optimize: bool = True
|
||
lto: bool = False # Link Time Optimization
|
||
|
||
# 平台特定选项
|
||
windows_icon: Optional[str] = None
|
||
windows_company_name: Optional[str] = None
|
||
windows_product_name: Optional[str] = None
|
||
windows_file_version: Optional[str] = None
|
||
windows_product_version: Optional[str] = None
|
||
|
||
macos_app_bundle: bool = False
|
||
macos_icon: Optional[str] = None
|
||
|
||
# 包含/排除选项
|
||
include_packages: List[str] = field(default_factory=list)
|
||
include_modules: List[str] = field(default_factory=list)
|
||
include_data_files: List[str] = field(default_factory=list)
|
||
include_data_dirs: List[Dict[str, str]] = field(default_factory=list)
|
||
|
||
nofollow_imports: List[str] = field(default_factory=list)
|
||
nofollow_import_to: List[str] = field(default_factory=list)
|
||
|
||
# 插件选项
|
||
enable_plugins: List[str] = field(default_factory=list)
|
||
disable_plugins: List[str] = field(default_factory=list)
|
||
|
||
# 额外参数
|
||
extra_args: List[str] = field(default_factory=list)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""转换为字典"""
|
||
data = asdict(self)
|
||
|
||
# 过滤掉值为 None 的字段,避免 TOML 序列化错误
|
||
filtered_data = {}
|
||
for key, value in data.items():
|
||
if value is not None:
|
||
filtered_data[key] = value
|
||
|
||
return filtered_data
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> 'NuitkaOptions':
|
||
"""从字典创建"""
|
||
# 过滤掉不存在的字段
|
||
valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
|
||
filtered_data = {k: v for k, v in data.items() if k in valid_fields}
|
||
return cls(**filtered_data)
|
||
|
||
|
||
@dataclass
|
||
class NativeCompilerOptions:
|
||
"""原生编译器选项 - Python → C/C++ → GCC/G++ → dll/so + exe"""
|
||
|
||
# 编译模式
|
||
mode: str = "native" # native | cython | cffi
|
||
|
||
# 编译器选择
|
||
cc: str = "gcc" # C 编译器
|
||
cxx: str = "g++" # C++ 编译器
|
||
|
||
# 编译选项
|
||
c_flags: List[str] = field(default_factory=lambda: ["-O2", "-fPIC"])
|
||
cxx_flags: List[str] = field(default_factory=lambda: ["-O2", "-fPIC", "-std=c++17"])
|
||
link_flags: List[str] = field(default_factory=list)
|
||
|
||
# 输出选项
|
||
output_dll: bool = True # 生成 dll/so
|
||
output_exe: bool = True # 生成 exe
|
||
output_static: bool = False # 生成静态库
|
||
|
||
# 嵌入 Python
|
||
embed_python: bool = True # 嵌入 Python 解释器
|
||
python_static: bool = False # 静态链接 Python
|
||
|
||
# 优化选项
|
||
lto: bool = False # Link Time Optimization
|
||
strip: bool = True # 剥离符号
|
||
|
||
# 调试选项
|
||
debug: bool = False # 调试模式
|
||
keep_c_source: bool = False # 保留生成的 C/C++ 源码
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""转换为字典"""
|
||
return asdict(self)
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> 'NativeCompilerOptions':
|
||
"""从字典创建"""
|
||
valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
|
||
filtered_data = {k: v for k, v in data.items() if k in valid_fields}
|
||
return cls(**filtered_data)
|
||
|
||
|
||
@dataclass
|
||
class BuildConfig:
|
||
"""Sikuwa 构建配置"""
|
||
|
||
# 项目基础信息
|
||
project_name: str
|
||
version: str = "1.0.0"
|
||
description: str = ""
|
||
author: str = ""
|
||
|
||
# 构建配置
|
||
main_script: str = "main.py"
|
||
src_dir: str = "."
|
||
output_dir: str = "dist"
|
||
build_dir: str = "build"
|
||
|
||
# 目标平台
|
||
platforms: List[str] = field(default_factory=lambda: ["windows"])
|
||
|
||
# 编译模式选择: "nuitka" | "native"
|
||
compiler_mode: str = "nuitka"
|
||
|
||
# Nuitka 选项 (compiler_mode="nuitka" 时使用)
|
||
nuitka_options: NuitkaOptions = field(default_factory=NuitkaOptions)
|
||
|
||
# 原生编译器选项 (compiler_mode="native" 时使用)
|
||
native_options: NativeCompilerOptions = field(default_factory=NativeCompilerOptions)
|
||
|
||
# 资源文件
|
||
resources: List[str] = field(default_factory=list)
|
||
|
||
# Python 环境
|
||
python_version: Optional[str] = None
|
||
python_path: Optional[str] = None
|
||
|
||
# 依赖管理
|
||
requirements_file: Optional[str] = None
|
||
pip_index_url: Optional[str] = None
|
||
dependencies: List[str] = field(default_factory=list)
|
||
|
||
# 编译序列配置
|
||
build_sequence: Optional[List[Dict[str, Any]]] = None
|
||
sequence_dependencies: Optional[Dict[str, List[str]]] = None
|
||
parallel_build: bool = False
|
||
max_workers: int = 4
|
||
|
||
# 钩子脚本
|
||
pre_build_script: Optional[str] = None
|
||
post_build_script: Optional[str] = None
|
||
|
||
def validate(self) -> None:
|
||
"""验证配置"""
|
||
if not self.project_name:
|
||
raise ValueError("project_name 不能为空")
|
||
|
||
# 如果是编译序列配置,跳过main_script验证
|
||
if not self.build_sequence:
|
||
if not self.main_script:
|
||
raise ValueError("main_script 不能为空")
|
||
|
||
valid_platforms = ["windows", "linux", "macos"]
|
||
for platform in self.platforms:
|
||
if platform not in valid_platforms:
|
||
raise ValueError(f"不支持的平台: {platform},有效平台: {valid_platforms}")
|
||
|
||
# 检查主脚本是否存在
|
||
main_file = Path(self.src_dir) / self.main_script
|
||
if not main_file.exists():
|
||
raise FileNotFoundError(f"主脚本不存在: {main_file}")
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""转换为字典"""
|
||
data = asdict(self)
|
||
data['nuitka_options'] = self.nuitka_options.to_dict()
|
||
data['native_options'] = self.native_options.to_dict()
|
||
|
||
# 过滤掉值为 None 的字段,避免 TOML 序列化错误
|
||
filtered_data = {}
|
||
for key, value in data.items():
|
||
if value is not None:
|
||
filtered_data[key] = value
|
||
|
||
return filtered_data
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> 'BuildConfig':
|
||
"""从字典创建"""
|
||
# 提取 nuitka_options
|
||
nuitka_data = data.pop('nuitka_options', {})
|
||
nuitka_options = NuitkaOptions.from_dict(nuitka_data)
|
||
|
||
# 提取 native_options
|
||
native_data = data.pop('native_options', {})
|
||
native_options = NativeCompilerOptions.from_dict(native_data)
|
||
|
||
# 过滤掉不存在的字段
|
||
valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
|
||
filtered_data = {k: v for k, v in data.items() if k in valid_fields}
|
||
|
||
return cls(nuitka_options=nuitka_options, native_options=native_options, **filtered_data)
|
||
|
||
@classmethod
|
||
def from_toml(cls, config_file: str) -> 'BuildConfig':
|
||
"""从 TOML 文件加载配置"""
|
||
config_path = Path(config_file)
|
||
|
||
if not config_path.exists():
|
||
raise FileNotFoundError(f"配置文件不存在: {config_file}")
|
||
|
||
try:
|
||
with open(config_path, 'rb') as f:
|
||
data = tomllib.load(f)
|
||
except Exception as e:
|
||
raise ValueError(f"解析 TOML 文件失败: {e}")
|
||
|
||
# 提取 [sikuwa] 部分
|
||
if 'sikuwa' not in data:
|
||
raise ValueError("配置文件缺少 [sikuwa] 部分")
|
||
|
||
sikuwa_config = data['sikuwa'].copy()
|
||
|
||
# 解析 nuitka 选项
|
||
nuitka_data = sikuwa_config.pop('nuitka', {})
|
||
|
||
# 处理可能存在的嵌套 nuitka_options
|
||
if 'nuitka_options' in sikuwa_config:
|
||
nuitka_data.update(sikuwa_config.pop('nuitka_options'))
|
||
|
||
nuitka_options = NuitkaOptions.from_dict(nuitka_data)
|
||
|
||
# 解析原生编译器选项
|
||
native_data = sikuwa_config.pop('native', {})
|
||
|
||
# 处理可能存在的嵌套 native_options
|
||
if 'native_options' in sikuwa_config:
|
||
native_data.update(sikuwa_config.pop('native_options'))
|
||
|
||
native_options = NativeCompilerOptions.from_dict(native_data)
|
||
|
||
# 创建配置对象
|
||
config = cls(nuitka_options=nuitka_options, native_options=native_options, **sikuwa_config)
|
||
|
||
return config
|
||
|
||
def save_to_toml(self, config_file: str) -> None:
|
||
"""保存配置到 TOML 文件"""
|
||
# 优先使用 tomli_w
|
||
try:
|
||
import tomli_w as toml_writer
|
||
use_binary = True
|
||
except ImportError:
|
||
try:
|
||
import toml as toml_writer
|
||
use_binary = False
|
||
except ImportError:
|
||
raise ImportError(
|
||
"需要安装 'tomli-w' 或 'toml' 包以保存 TOML 文件:\n"
|
||
" pip install tomli-w\n"
|
||
"或\n"
|
||
" pip install toml"
|
||
)
|
||
|
||
data = {
|
||
'sikuwa': self.to_dict()
|
||
}
|
||
|
||
config_path = Path(config_file)
|
||
|
||
try:
|
||
if use_binary:
|
||
with open(config_path, 'wb') as f:
|
||
toml_writer.dump(data, f)
|
||
else:
|
||
with open(config_path, 'w', encoding='utf-8') as f:
|
||
toml_writer.dump(data, f)
|
||
except Exception as e:
|
||
raise IOError(f"保存配置文件失败: {e}")
|
||
|
||
|
||
class ConfigManager:
|
||
"""配置管理器"""
|
||
|
||
DEFAULT_CONFIG_FILES = [
|
||
"sikuwa.toml",
|
||
"pyproject.toml",
|
||
".sikuwa.toml"
|
||
]
|
||
|
||
@staticmethod
|
||
def find_config() -> Optional[Path]:
|
||
"""自动查找配置文件"""
|
||
for config_file in ConfigManager.DEFAULT_CONFIG_FILES:
|
||
config_path = Path(config_file)
|
||
if config_path.exists():
|
||
return config_path
|
||
return None
|
||
|
||
@staticmethod
|
||
def load_config(config_file: Optional[str] = None) -> BuildConfig:
|
||
"""加载配置文件"""
|
||
if config_file:
|
||
return BuildConfig.from_toml(config_file)
|
||
|
||
# 自动查找配置文件
|
||
config_path = ConfigManager.find_config()
|
||
if config_path:
|
||
return BuildConfig.from_toml(str(config_path))
|
||
|
||
raise FileNotFoundError(
|
||
"未找到配置文件,请创建以下文件之一:\n " +
|
||
"\n ".join(ConfigManager.DEFAULT_CONFIG_FILES) +
|
||
"\n\n使用命令创建默认配置:\n sikuwa init"
|
||
)
|
||
|
||
@staticmethod
|
||
def create_default_config(output_file: str = "sikuwa.toml") -> None:
|
||
"""创建默认配置文件"""
|
||
default_config = BuildConfig(
|
||
project_name="my_project",
|
||
version="1.0.0",
|
||
description="My Python Project",
|
||
author="",
|
||
main_script="main.py",
|
||
src_dir=".",
|
||
output_dir="dist",
|
||
build_dir="build",
|
||
platforms=["windows"],
|
||
nuitka_options=NuitkaOptions(
|
||
standalone=True,
|
||
onefile=False,
|
||
follow_imports=True,
|
||
show_progress=True,
|
||
enable_console=True,
|
||
optimize=True,
|
||
include_packages=[],
|
||
nofollow_import_to=[
|
||
"numpy",
|
||
"pandas",
|
||
"matplotlib"
|
||
]
|
||
),
|
||
resources=[],
|
||
dependencies=["requests>=2.0.0", "click>=8.0.0"]
|
||
)
|
||
|
||
try:
|
||
default_config.save_to_toml(output_file)
|
||
print(f"✓ 已创建默认配置文件: {output_file}")
|
||
except Exception as e:
|
||
print(f"✗ 创建配置文件失败: {e}")
|
||
raise
|
||
|
||
|
||
# 便捷函数
|
||
def load_config(config_file: Optional[str] = None) -> BuildConfig:
|
||
"""加载配置(便捷函数)"""
|
||
return ConfigManager.load_config(config_file)
|
||
|
||
|
||
def create_config(output_file: str = "sikuwa.toml") -> None:
|
||
"""创建默认配置(便捷函数)"""
|
||
ConfigManager.create_default_config(output_file)
|
||
|
||
|
||
def validate_config(config: BuildConfig) -> List[str]:
|
||
"""验证配置有效性,返回错误列表"""
|
||
errors = []
|
||
|
||
try:
|
||
config.validate()
|
||
except Exception as e:
|
||
errors.append(str(e))
|
||
|
||
# 额外检查
|
||
src_dir = Path(config.src_dir)
|
||
if not src_dir.exists():
|
||
errors.append(f"源码目录不存在: {config.src_dir}")
|
||
|
||
if config.nuitka_options.windows_icon:
|
||
icon_path = Path(config.nuitka_options.windows_icon)
|
||
if not icon_path.exists():
|
||
errors.append(f"图标文件不存在: {config.nuitka_options.windows_icon}")
|
||
|
||
return errors
|
||
|
||
|
||
"""Sikuwa 配置管理"""
|
||
|
||
from pathlib import Path
|
||
from typing import List, Dict, Optional
|
||
|
||
|
||
class SikuwaConfig:
|
||
"""Sikuwa 项目配置"""
|
||
|
||
def __init__(self, config_path: Path = None):
|
||
if config_path is None:
|
||
config_path = Path("sikuwa.toml")
|
||
|
||
self.config_path = config_path
|
||
self._load_config()
|
||
|
||
def _load_config(self):
|
||
"""加载配置文件"""
|
||
if not self.config_path.exists():
|
||
raise FileNotFoundError(f"配置文件不存在: {self.config_path}")
|
||
|
||
with open(self.config_path, 'rb') as f:
|
||
data = tomllib.load(f)
|
||
|
||
# 基础配置
|
||
sikuwa = data.get('sikuwa', {})
|
||
self.project_name = sikuwa.get('project_name', 'my_project')
|
||
self.version = sikuwa.get('version', '1.0.0')
|
||
self.main_script = Path(sikuwa.get('main_script', 'main.py'))
|
||
self.src_dir = Path(sikuwa.get('src_dir', '.'))
|
||
self.output_dir = Path(sikuwa.get('output_dir', 'dist'))
|
||
self.build_dir = Path(sikuwa.get('build_dir', 'build'))
|
||
self.platforms = sikuwa.get('platforms', ['windows'])
|
||
|
||
# Nuitka 配置
|
||
nuitka = data.get('sikuwa', {}).get('nuitka', {})
|
||
self.standalone = nuitka.get('standalone', True)
|
||
self.onefile = nuitka.get('onefile', False)
|
||
self.follow_imports = nuitka.get('follow_imports', True)
|
||
self.show_progress = nuitka.get('show_progress', True)
|
||
self.enable_console = nuitka.get('enable_console', True)
|
||
|
||
self.include_packages = nuitka.get('include_packages', [])
|
||
self.include_data_files = nuitka.get('include_data_files', [])
|
||
self.include_data_dirs = nuitka.get('include_data_dirs', []) # 新增
|
||
|
||
self.extra_args = nuitka.get('extra_args', [])
|
||
|
||
def __repr__(self):
|
||
return f"<SikuwaConfig project={self.project_name} version={self.version}>"
|
||
|
||
if __name__ == '__main__':
|
||
# 测试配置模块
|
||
print("Sikuwa Config - 测试模式")
|
||
print("=" * 70)
|
||
|
||
# 创建测试配置
|
||
test_config = BuildConfig(
|
||
project_name="test_app",
|
||
version="0.1.0",
|
||
main_script="main.py",
|
||
platforms=["windows", "linux"],
|
||
nuitka_options=NuitkaOptions(
|
||
standalone=True,
|
||
onefile=True,
|
||
follow_imports=True,
|
||
include_packages=["requests", "click"],
|
||
windows_icon="icon.ico",
|
||
nofollow_import_to=["numpy", "pandas"]
|
||
),
|
||
resources=["config.json", "data/"]
|
||
)
|
||
|
||
print("\n测试配置对象:")
|
||
print(f" 项目名称: {test_config.project_name}")
|
||
print(f" 版本: {test_config.version}")
|
||
print(f" 目标平台: {test_config.platforms}")
|
||
print(f" Standalone: {test_config.nuitka_options.standalone}")
|
||
print(f" OneFile: {test_config.nuitka_options.onefile}")
|
||
|
||
# 测试保存和加载
|
||
test_file = "test_sikuwa.toml"
|
||
try:
|
||
print(f"\n保存配置到: {test_file}")
|
||
test_config.save_to_toml(test_file)
|
||
|
||
print(f"从文件加载配置: {test_file}")
|
||
loaded_config = BuildConfig.from_toml(test_file)
|
||
|
||
print("\n加载的配置:")
|
||
print(f" 项目名称: {loaded_config.project_name}")
|
||
print(f" 版本: {loaded_config.version}")
|
||
print(f" 目标平台: {loaded_config.platforms}")
|
||
print(f" 包含包: {loaded_config.nuitka_options.include_packages}")
|
||
print(f" 排除包: {loaded_config.nuitka_options.nofollow_import_to}")
|
||
|
||
print("\n✓ 配置模块测试通过!")
|
||
|
||
except Exception as e:
|
||
print(f"\n✗ 测试失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
finally:
|
||
# 清理测试文件
|
||
import os
|
||
if os.path.exists(test_file):
|
||
os.remove(test_file)
|
||
print(f"\n清理测试文件: {test_file}")
|