Files
Sikuwa/config.py
so陈 13a1072c6f
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
Sikuwa first commit
2026-02-20 23:53:48 +08:00

529 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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}")