Files
Sikuwa/cli.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

829 lines
25 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/cli.py
"""
Sikuwa 命令行界面
"""
import click
import sys
from pathlib import Path
from typing import Optional
from sikuwa.config import ConfigManager, BuildConfig, create_config
from sikuwa.builder import SikuwaBuilder, build_project, clean_project, sync_project
from sikuwa.log import get_logger, LogLevel
from sikuwa.i18n import _
# 全局日志器
logger = get_logger("sikuwa.cli")
@click.group()
@click.version_option(version="1.2.0", prog_name="sikuwa")
def cli():
"""
Sikuwa - Python 项目打包工具
基于 Nuitka 的跨平台 Python 应用构建工具
"""
pass
@cli.command()
@click.option(
'-c', '--config',
type=click.Path(exists=True),
help='配置文件路径 (默认: sikuwa.toml)'
)
@click.option(
'-p', '--platform',
type=click.Choice(['windows', 'linux', 'macos'], case_sensitive=False),
help='目标平台 (默认: 构建所有平台)'
)
@click.option(
'-m', '--mode',
type=click.Choice(['nuitka', 'native'], case_sensitive=False),
help='编译模式: nuitka (默认) | native (Python→C/C++→GCC→dll/so+exe)'
)
@click.option(
'-v', '--verbose',
is_flag=True,
help=_('详细输出模式')
)
@click.option(
'-f', '--force',
is_flag=True,
help=_('强制重新构建')
)
@click.option(
'--keep-c-source',
is_flag=True,
help='保留生成的 C/C++ 源码 (仅 native 模式)'
)
def build(config: Optional[str], platform: Optional[str], mode: Optional[str],
verbose: bool, force: bool, keep_c_source: bool):
"""
构建项目
支持两种编译模式:
\b
1. nuitka (默认): 使用 Nuitka 编译器
2. native: Python → C/C++ → GCC/G++ → dll/so + exe
生成通用动态链接库,不使用 Python 专用格式 (.pyd)
示例:
sikuwa build # 使用 Nuitka 构建
sikuwa build -m native # 使用原生编译器构建
sikuwa build -m native -v # 原生编译 + 详细输出
sikuwa build -p windows # 只构建 Windows 平台
sikuwa build -c my_config.toml # 使用指定配置文件
"""
try:
# 加载配置
logger.info_operation("加载构建配置...")
build_config = ConfigManager.load_config(config)
# 如果命令行指定了编译模式,覆盖配置文件中的设置
if mode:
build_config.compiler_mode = mode
if mode == 'native' and keep_c_source:
build_config.native_options.keep_c_source = True
# 验证配置
logger.info_operation("验证配置...")
build_config.validate()
# 执行构建
logger.info_operation(f"开始构建项目: {build_config.project_name}")
logger.info_operation(f"编译模式: {build_config.compiler_mode.upper()}")
success = build_project(
config=build_config,
platform=platform,
verbose=verbose,
force=force
)
if success:
click.echo("\n[OK] 构建成功完成!", err=False)
sys.exit(0)
else:
click.echo("\n[FAIL] 构建失败!", err=True)
sys.exit(1)
except FileNotFoundError as e:
click.echo(f"[FAIL] {e}", err=True)
click.echo("\n提示: 使用 'sikuwa init' 创建配置文件", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"[FAIL] 构建失败: {e}", err=True)
if verbose:
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
@cli.command()
@click.option(
'-c', '--config',
type=click.Path(exists=True),
help='配置文件路径 (默认: sikuwa.toml)'
)
@click.option(
'-v', '--verbose',
is_flag=True,
help=_('详细输出模式')
)
def clean(config: Optional[str], verbose: bool):
"""
清理构建文件
删除构建过程中生成的所有文件和目录
示例:
sikuwa clean # 清理构建文件
sikuwa clean -v # 详细输出
"""
try:
# 加载配置
logger.info_operation("加载配置...")
build_config = ConfigManager.load_config(config)
# 执行清理
logger.info_operation("开始清理构建文件...")
success = clean_project(
config=build_config,
verbose=verbose
)
if success:
click.echo("\n[OK] 清理完成!", err=False)
sys.exit(0)
else:
click.echo("\n[FAIL] 清理失败!", err=True)
sys.exit(1)
except FileNotFoundError as e:
click.echo(f"[FAIL] {e}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"[FAIL] 清理失败: {e}", err=True)
if verbose:
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
@cli.command()
@click.option(
'-o', '--output',
default='sikuwa.toml',
help='输出配置文件名 (默认: sikuwa.toml)'
)
@click.option(
'--force',
is_flag=True,
help=_('覆盖已存在的文件')
)
def init(output: str, force: bool):
"""
初始化项目配置
创建默认的 sikuwa.toml 配置文件
示例:
sikuwa init # 创建 sikuwa.toml
sikuwa init -o custom.toml # 创建自定义配置文件
sikuwa init --force # 强制覆盖已存在的文件
"""
try:
output_path = Path(output)
# 检查文件是否已存在
if output_path.exists() and not force:
click.echo(f"[WARN] 配置文件已存在: {output}", err=True)
click.echo("使用 --force 选项强制覆盖", err=True)
sys.exit(1)
# 创建配置文件
logger.info_operation(f"创建配置文件: {output}")
create_config(output)
click.echo(f"\n[OK] 配置文件已创建: {output}")
click.echo("\n下一步:")
click.echo(" 1. 编辑 sikuwa.toml配置项目信息")
click.echo(" 2. 运行 'sikuwa build' 开始构建")
sys.exit(0)
except Exception as e:
click.echo(f"[FAIL] 创建配置文件失败: {e}", err=True)
sys.exit(1)
@cli.command()
@click.option(
'-c', '--config',
type=click.Path(exists=True),
help='配置文件路径 (默认: sikuwa.toml)'
)
def info(config: Optional[str]):
"""
显示项目信息
显示当前项目的配置信息
示例:
sikuwa info # 显示项目信息
sikuwa info -c custom.toml # 显示指定配置文件的信息
"""
try:
# 加载配置
build_config = ConfigManager.load_config(config)
# 显示项目信息
click.echo("\n" + "=" * 70)
click.echo(f"项目信息: {build_config.project_name}")
click.echo("=" * 70)
click.echo(f"\n基础信息:")
click.echo(f" 项目名称: {build_config.project_name}")
click.echo(f" 版本: {build_config.version}")
if build_config.description:
click.echo(f" 描述: {build_config.description}")
if build_config.author:
click.echo(f" 作者: {build_config.author}")
click.echo(f"\n构建配置:")
click.echo(f" 入口文件: {build_config.main_script}")
click.echo(f" 源代码目录: {build_config.src_dir}")
click.echo(f" 输出目录: {build_config.output_dir}")
click.echo(f" 构建目录: {build_config.build_dir}")
click.echo(f" 目标平台: {', '.join(build_config.platforms)}")
click.echo(f"\nNuitka 选项:")
click.echo(f" Standalone: {build_config.nuitka_options.standalone}")
click.echo(f" OneFile: {build_config.nuitka_options.onefile}")
click.echo(f" Follow Imports: {build_config.nuitka_options.follow_imports}")
click.echo(f" Show Progress: {build_config.nuitka_options.show_progress}")
click.echo(f" Enable Console: {build_config.nuitka_options.enable_console}")
if build_config.nuitka_options.include_packages:
click.echo(f"\n包含的包:")
for pkg in build_config.nuitka_options.include_packages:
click.echo(f" - {pkg}")
if build_config.resources:
click.echo(f"\n资源文件:")
for resource in build_config.resources:
click.echo(f" - {resource}")
click.echo("\n" + "=" * 70 + "\n")
except FileNotFoundError as e:
click.echo(f"[FAIL] {e}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"[FAIL] 读取配置失败: {e}", err=True)
sys.exit(1)
@cli.command()
def version():
"""
显示版本信息
"""
click.echo("\nSikuwa v1.2.0")
click.echo("Python 项目打包工具")
click.echo("基于 Nuitka 的跨平台构建系统")
click.echo("\nGitHub: https://github.com/FORGE24/Sikuwa/")
click.echo("文档: https://www.sanrol-cloud.top\n")
@cli.command()
@click.option(
'-c', '--config',
type=click.Path(exists=True),
help='配置文件路径 (默认: sikuwa.toml)'
)
def validate(config: Optional[str]):
"""
验证配置文件
检查配置文件是否正确
示例:
sikuwa validate # 验证默认配置文件
sikuwa validate -c custom.toml # 验证指定配置文件
"""
try:
# 加载配置
logger.info_operation("加载配置文件...")
build_config = ConfigManager.load_config(config)
# 验证配置
logger.info_operation("验证配置...")
build_config.validate()
click.echo("\n[OK] 配置文件有效!")
# 显示摘要信息
click.echo(f"\n项目: {build_config.project_name}")
click.echo(f"版本: {build_config.version}")
click.echo(f"入口: {build_config.main_script}")
click.echo(f"平台: {', '.join(build_config.platforms)}\n")
sys.exit(0)
except FileNotFoundError as e:
click.echo(f"[FAIL] {e}", err=True)
sys.exit(1)
except ValueError as e:
click.echo(f"[FAIL] 配置验证失败: {e}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"[FAIL] 验证失败: {e}", err=True)
sys.exit(1)
@cli.command()
def doctor():
"""
检查构建环境
检查系统环境和依赖项是否满足构建要求
"""
import subprocess
import platform
click.echo("\n" + "=" * 70)
click.echo("Sikuwa 环境诊断")
click.echo("=" * 70)
# 检查 Python 版本
click.echo("\n[1] Python 环境")
python_version = sys.version_info
click.echo(f" 版本: {python_version.major}.{python_version.minor}.{python_version.micro}")
click.echo(f" 路径: {sys.executable}")
if python_version.major < 3 or (python_version.major == 3 and python_version.minor < 7):
click.echo(" [FAIL] Python 版本过低,需要 3.7+", err=True)
else:
click.echo(" [OK] Python 版本满足要求")
# 检查操作系统
click.echo("\n[2] 操作系统")
os_name = platform.system()
os_version = platform.version()
click.echo(f" 系统: {os_name}")
click.echo(f" 版本: {os_version}")
click.echo(f" 架构: {platform.machine()}")
# 检查 Nuitka
click.echo("\n[3] Nuitka")
try:
result = subprocess.run(
["nuitka3", "--version"],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
version = result.stdout.strip().split('\n')[0]
click.echo(f" [OK] 已安装: {version}")
else:
click.echo(" [FAIL] Nuitka 未正确安装", err=True)
except FileNotFoundError:
click.echo(" [FAIL] Nuitka 未安装", err=True)
click.echo(" 安装命令: pip install nuitka")
except Exception as e:
click.echo(f" [WARN] 检查 Nuitka 时出错: {e}", err=True)
# 检查编译器 (Windows)
if os_name == "Windows":
click.echo("\n[4] C 编译器 (Windows)")
# 检查 MinGW
try:
result = subprocess.run(
["gcc", "--version"],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
version = result.stdout.strip().split('\n')[0]
click.echo(f" [OK] GCC 已安装: {version}")
else:
click.echo(" [WARN] GCC 未找到", err=True)
except FileNotFoundError:
click.echo(" [WARN] GCC 未安装", err=True)
click.echo(" 推荐安装 MinGW-w64 或 MSVC")
except Exception as e:
click.echo(f" [WARN] 检查 GCC 时出错: {e}", err=True)
# 检查 MSVC
try:
result = subprocess.run(
["cl"],
capture_output=True,
text=True,
timeout=5
)
if "Microsoft" in result.stderr or "Microsoft" in result.stdout:
click.echo(" [OK] MSVC 已安装")
else:
click.echo(" [INFO] MSVC 未找到")
except FileNotFoundError:
click.echo(" [INFO] MSVC 未安装")
except Exception as e:
click.echo(f" [INFO] 检查 MSVC 时出错: {e}")
# 检查编译器 (Linux/macOS)
elif os_name in ["Linux", "Darwin"]:
click.echo(f"\n[4] C 编译器 ({os_name})")
try:
result = subprocess.run(
["gcc", "--version"],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
version = result.stdout.strip().split('\n')[0]
click.echo(f" [OK] GCC 已安装: {version}")
else:
click.echo(" [FAIL] GCC 未找到", err=True)
except FileNotFoundError:
click.echo(" [FAIL] GCC 未安装", err=True)
if os_name == "Linux":
click.echo(" 安装命令: sudo apt install gcc # Debian/Ubuntu")
click.echo(" sudo yum install gcc # RedHat/CentOS")
elif os_name == "Darwin":
click.echo(" 安装命令: xcode-select --install")
except Exception as e:
click.echo(f" [WARN] 检查 GCC 时出错: {e}", err=True)
# 检查必需的 Python 包
click.echo("\n[5] Python 依赖包")
required_packages = {
'click': 'CLI 框架',
'tomli': 'TOML 解析器',
'tomli_w': 'TOML 写入器',
'nuitka': 'Python 编译器'
}
for package, description in required_packages.items():
try:
__import__(package)
click.echo(f" [OK] {package:15s} - {description}")
except ImportError:
click.echo(f" [FAIL] {package:15s} - {description} (未安装)", err=True)
# 检查可选包
click.echo("\n[6] 可选依赖包")
optional_packages = {
'ordered_set': _('有序集合支持'),
'zstandard': 'Zstandard 压缩',
}
for package, description in optional_packages.items():
try:
__import__(package)
click.echo(f" [OK] {package:15s} - {description}")
except ImportError:
click.echo(f" [INFO] {package:15s} - {description} (未安装)")
# 总结
click.echo("\n" + "=" * 70)
click.echo(_("诊断完成"))
click.echo("=" * 70)
click.echo("\n如果有 [FAIL] 项,请先解决这些问题后再进行构建。\n")
@cli.command()
@click.argument('query', required=False)
def help_cmd(query: Optional[str]):
"""
显示帮助信息
示例:
sikuwa help # 显示总体帮助
sikuwa help build # 显示 build 命令帮助
sikuwa help config # 显示配置文件帮助
"""
if not query:
# 显示总体帮助
click.echo("\n" + "=" * 70)
click.echo("Sikuwa - Python 项目打包工具")
click.echo("=" * 70)
click.echo("\n常用命令:")
click.echo(" sikuwa init 创建配置文件")
click.echo(" sikuwa build 构建项目")
click.echo(" sikuwa clean 清理构建文件")
click.echo(" sikuwa sync 同步项目依赖")
click.echo(" sikuwa info 显示项目信息")
click.echo(" sikuwa doctor 检查构建环境")
click.echo("\n获取更多帮助:")
click.echo(" sikuwa --help 显示所有命令")
click.echo(" sikuwa <command> --help 显示命令详细帮助")
click.echo(" sikuwa help config 配置文件帮助")
click.echo("\n快速开始:")
click.echo(" 1. sikuwa init # 创建配置文件")
click.echo(" 2. 编辑 sikuwa.toml # 配置项目")
click.echo(" 3. sikuwa sync # 同步项目依赖")
click.echo(" 4. sikuwa build # 构建项目")
click.echo("\n文档: https://www.sanrol-cloud.top")
click.echo("=" * 70 + "\n")
elif query.lower() == "config":
# 配置文件帮助
click.echo("\n" + "=" * 70)
click.echo("Sikuwa 配置文件说明")
click.echo("=" * 70)
click.echo("\n配置文件示例 (sikuwa.toml):\n")
click.echo("""[sikuwa]
project_name = "my_app"
version = "1.0.0"
description = "My Application"
author = "Your Name"
main_script = "main.py"
src_dir = "."
output_dir = "dist"
build_dir = "build"
platforms = ["windows", "linux"]
resources = ["config.json", "data/"]
[sikuwa.nuitka]
standalone = true
onefile = false
follow_imports = true
show_progress = true
enable_console = true
optimize = true
include_packages = ["requests", "numpy"]
include_modules = []
include_data_files = []
include_data_dirs = []
windows_icon = "icon.ico"
windows_company_name = "My Company"
windows_product_name = "My Product"
""")
click.echo("\n主要配置项:")
click.echo(" project_name 项目名称")
click.echo(" main_script 入口文件")
click.echo(" platforms 目标平台 (windows/linux/macos)")
click.echo(" standalone 独立模式")
click.echo(" onefile 单文件模式")
click.echo(" include_packages 包含的 Python 包")
click.echo("\n详细文档: https://www.sanrol-cloud.top")
click.echo("=" * 70 + "\n")
else:
# 显示特定命令的帮助
ctx = click.Context(cli)
cmd = cli.get_command(ctx, query)
if cmd:
click.echo(cmd.get_help(ctx))
else:
click.echo(f"[FAIL] 未知命令: {query}", err=True)
click.echo("使用 'sikuwa --help' 查看所有可用命令", err=True)
@cli.command()
@click.option(
'-c', '--config',
type=click.Path(exists=True),
help='配置文件路径 (默认: sikuwa.toml)'
)
@click.option(
'--format',
type=click.Choice(['text', 'json'], case_sensitive=False),
default='text',
help='输出格式 (默认: text)'
)
def show_config(config: Optional[str], format: str):
"""
显示完整配置
以易读的格式显示所有配置选项
示例:
sikuwa show-config # 显示配置 (文本格式)
sikuwa show-config --format json # 显示配置 (JSON 格式)
"""
try:
# 加载配置
build_config = ConfigManager.load_config(config)
if format == 'json':
# JSON 格式输出
import json
config_dict = build_config.to_dict()
click.echo(json.dumps(config_dict, indent=2, ensure_ascii=False))
else:
# 文本格式输出
config_dict = build_config.to_dict()
click.echo("\n" + "=" * 70)
click.echo(_("完整配置"))
click.echo("=" * 70)
def print_dict(d, indent=0):
for key, value in d.items():
if isinstance(value, dict):
click.echo(" " * indent + f"{key}:")
print_dict(value, indent + 1)
elif isinstance(value, list):
click.echo(" " * indent + f"{key}:")
for item in value:
click.echo(" " * (indent + 1) + f"- {item}")
else:
click.echo(" " * indent + f"{key}: {value}")
print_dict(config_dict)
click.echo("=" * 70 + "\n")
sys.exit(0)
except FileNotFoundError as e:
click.echo(f"[FAIL] {e}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"[FAIL] 读取配置失败: {e}", err=True)
sys.exit(1)
@cli.command()
@click.option(
'-c', '--config',
type=click.Path(exists=True),
help='配置文件路径 (默认: sikuwa.toml)'
)
@click.option(
'-v', '--verbose',
is_flag=True,
help=_('详细输出模式')
)
def sync(config: Optional[str], verbose: bool):
"""
同步项目依赖
自动创建或进入虚拟环境,并安装配置文件中指定的依赖包
示例:
sikuwa sync # 同步依赖
sikuwa sync -v # 详细输出
sikuwa sync -c my_config.toml # 使用指定配置文件
"""
try:
from sikuwa.builder import sync_project
# 加载配置
logger.info_operation("加载构建配置...")
build_config = ConfigManager.load_config(config)
# 验证配置
logger.info_operation("验证配置...")
build_config.validate()
# 执行同步
logger.info_operation(f"开始同步项目依赖: {build_config.project_name}")
success = sync_project(
config=build_config,
verbose=verbose
)
if success:
click.echo("\n[OK] 依赖同步成功完成!", err=False)
sys.exit(0)
else:
click.echo("\n[FAIL] 依赖同步失败!", err=True)
sys.exit(1)
except FileNotFoundError as e:
click.echo(f"[FAIL] {e}", err=True)
click.echo("\n提示: 使用 'sikuwa init' 创建配置文件", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"[FAIL] 同步失败: {e}", err=True)
if verbose:
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
@cli.command()
@click.option(
'-c', '--config',
type=click.Path(exists=True),
help='配置文件路径 (默认: sikuwa.toml)'
)
@click.option(
'-v', '--verbose',
is_flag=True,
help='详细输出模式'
)
def build_sequence(config: Optional[str], verbose: bool):
"""
执行编译序列构建
支持按配置文件中定义的项目依赖关系进行拓扑排序,并可选择并行构建
示例:
sikuwa build-sequence # 执行编译序列构建
sikuwa build-sequence -v # 详细输出
sikuwa build-sequence -c my_config.toml # 使用指定配置文件
"""
try:
from sikuwa.builder import build_sequence
# 加载配置
logger.info_operation("加载构建配置...")
build_config = ConfigManager.load_config(config)
# 验证配置
logger.info_operation("验证配置...")
build_config.validate()
# 执行编译序列构建
success = build_sequence(
config=build_config,
verbose=verbose
)
if success:
click.echo("\n[OK] 编译序列构建成功完成!", err=False)
sys.exit(0)
else:
click.echo("\n[FAIL] 编译序列构建失败!", err=True)
sys.exit(1)
except FileNotFoundError as e:
click.echo(f"[FAIL] {e}", err=True)
click.echo("\n提示: 使用 'sikuwa init' 创建配置文件", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"[FAIL] 构建失败: {e}", err=True)
if verbose:
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
def main():
"""主入口函数"""
try:
cli()
except KeyboardInterrupt:
click.echo("\n\n[WARN] 用户中断操作", err=True)
sys.exit(130)
except Exception as e:
click.echo(f"\n[FAIL] 未预期的错误: {e}", err=True)
import traceback
click.echo(traceback.format_exc(), err=True)
sys.exit(1)
if __name__ == '__main__':
main()