feat(create_project): 增强项目创建工具并添加开发环境检测

- 添加 DevToolsChecker 类用于检测开发环境工具链
- 自动克隆引擎源码到项目目录
- 添加系统字体自动复制功能
- 重构 xmake.lua 模板以支持本地引擎源码构建
- 改进 README.md 模板结构和内容
- 添加开发工具链下载提示功能
This commit is contained in:
ChestnutYueyue 2026-02-13 21:50:25 +08:00
parent 06de1e79af
commit cd6c65a555
1 changed files with 375 additions and 31 deletions

View File

@ -11,12 +11,109 @@ Extra2D 项目脚手架工具
python create_project.py my_game python create_project.py my_game
python create_project.py my_game --path ./games python create_project.py my_game --path ./games
python create_project.py my_game --author "Your Name" python create_project.py my_game --author "Your Name"
项目结构:
my_game/
src/
main.cpp
romfs/
assets/
xmake.lua
README.md
Extra2D/ # 引擎源码(自动克隆)
Extra2D/
xmake/
...
""" """
import os import os
import sys import sys
import argparse import argparse
import subprocess
import platform
import shutil
from pathlib import Path from pathlib import Path
from typing import Optional
ENGINE_REPO = "https://github.com/ChestnutYueyue/Extra2D.git"
DEVKITPRO_URL = "https://github.com/devkitPro/installer/releases/download/v3.0.3/devkitProUpdater-3.0.3.exe"
MINGW_URL = "https://github.com/brechtsanders/winlibs_mingw/releases/download/16.0.0-snapshot20251026posix-14.0.0-ucrt-r1/winlibs-i686-posix-dwarf-gcc-16.0.0-snapshot20251026-mingw-w64ucrt-14.0.0-r1.zip"
class DevToolsChecker:
"""开发工具检测器"""
def __init__(self):
self.is_windows = platform.system() == "Windows"
def check_git(self) -> bool:
"""检查 Git 是否安装"""
return shutil.which("git") is not None
def check_xmake(self) -> bool:
"""检查 xmake 是否安装"""
return shutil.which("xmake") is not None
def check_mingw(self) -> bool:
"""检查 MinGW 是否安装"""
if shutil.which("gcc"):
result = subprocess.run(
["gcc", "-dumpmachine"],
capture_output=True,
text=True,
creationflags=subprocess.CREATE_NO_WINDOW if self.is_windows else 0
)
return "mingw" in result.stdout.lower()
return False
def check_devkitpro(self) -> bool:
"""检查 devkitPro 是否安装"""
devkitpro = os.environ.get("DEVKITPRO", "C:/devkitPro")
devkita64 = os.path.join(devkitpro, "devkitA64")
return os.path.isdir(devkita64)
def get_missing_tools(self) -> dict:
"""获取缺失的工具列表"""
missing = {}
if not self.check_git():
missing["git"] = "Git 版本控制工具"
if not self.check_xmake():
missing["xmake"] = "xmake 构建工具"
return missing
def get_missing_dev_tools(self) -> dict:
"""获取缺失的开发工具链"""
missing = {}
if not self.check_mingw():
missing["mingw"] = MINGW_URL
if not self.check_devkitpro():
missing["devkitpro"] = DEVKITPRO_URL
return missing
def print_tool_status(self):
"""打印工具状态"""
print("\n========================================")
print("开发环境检测")
print("========================================")
tools = [
("Git", self.check_git()),
("xmake", self.check_xmake()),
("MinGW (Windows开发)", self.check_mingw()),
("devkitPro (Switch开发)", self.check_devkitpro()),
]
for name, installed in tools:
status = "✓ 已安装" if installed else "✗ 未安装"
print(f" {name}: {status}")
print("========================================\n")
class ProjectCreator: class ProjectCreator:
@ -25,14 +122,13 @@ class ProjectCreator:
def __init__(self, project_name: str, output_path: str = None, author: str = "Extra2D Team"): def __init__(self, project_name: str, output_path: str = None, author: str = "Extra2D Team"):
self.project_name = project_name self.project_name = project_name
self.author = author self.author = author
self.script_dir = Path(__file__).parent.resolve() self.output_path = Path(output_path) if output_path else Path.cwd()
self.engine_root = self.script_dir.parent
self.output_path = Path(output_path) if output_path else self.engine_root / "projects"
self.project_path = self.output_path / project_name self.project_path = self.output_path / project_name
self.engine_path = self.project_path / "Extra2D"
def create(self) -> bool: def create(self) -> bool:
"""创建项目""" """创建项目"""
print(f"正在创建 Extra2D 项目: {self.project_name}") print(f"\n正在创建 Extra2D 项目: {self.project_name}")
print(f"项目路径: {self.project_path}") print(f"项目路径: {self.project_path}")
if self.project_path.exists(): if self.project_path.exists():
@ -41,17 +137,20 @@ class ProjectCreator:
try: try:
self._create_directories() self._create_directories()
self._copy_system_font()
self._create_main_cpp() self._create_main_cpp()
self._create_xmake_lua() self._create_xmake_lua()
self._create_gitignore() self._create_gitignore()
self._create_readme() self._create_readme()
print(f"\n项目创建成功!") print(f"\n✓ 项目创建成功!")
print(f"\n后续步骤:")
print(f" 1. cd {self.project_path}") if self._clone_engine():
print(f" 2. xmake config") self._print_next_steps()
print(f" 3. xmake build") else:
print(f" 4. xmake run {self.project_name}") print("\n请手动克隆引擎源码:")
print(f" cd {self.project_path}")
print(f" git clone {ENGINE_REPO} Extra2D")
return True return True
except Exception as e: except Exception as e:
@ -66,6 +165,61 @@ class ProjectCreator:
(self.project_path / "romfs" / "assets" / "audio").mkdir(parents=True, exist_ok=True) (self.project_path / "romfs" / "assets" / "audio").mkdir(parents=True, exist_ok=True)
(self.project_path / "src").mkdir(parents=True, exist_ok=True) (self.project_path / "src").mkdir(parents=True, exist_ok=True)
def _copy_system_font(self):
"""复制系统字体到项目 assets 目录"""
print("复制系统字体...")
font_dest = self.project_path / "romfs" / "assets" / "font.ttf"
if platform.system() == "Windows":
font_sources = [
Path("C:/Windows/Fonts/msyh.ttc"), # 微软雅黑
Path("C:/Windows/Fonts/msyh.ttf"), # 微软雅黑 (部分系统)
Path("C:/Windows/Fonts/simhei.ttf"), # 黑体
Path("C:/Windows/Fonts/simsun.ttc"), # 宋体
]
else:
font_sources = [
Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"),
Path("/usr/share/fonts/TTF/DejaVuSans.ttf"),
Path("/System/Library/Fonts/PingFang.ttc"), # macOS
]
for font_src in font_sources:
if font_src.exists():
try:
shutil.copy2(str(font_src), str(font_dest))
print(f"✓ 已复制字体: {font_src.name}")
return True
except Exception as e:
print(f"✗ 复制字体失败: {e}")
continue
print("⚠ 未找到系统字体,请手动添加字体文件到 romfs/assets/font.ttf")
return False
def _clone_engine(self) -> bool:
"""克隆引擎源码到项目目录内的 Extra2D 子目录"""
print(f"\n正在克隆 Extra2D 引擎源码...")
print(f"仓库地址: {ENGINE_REPO}")
print(f"目标路径: {self.engine_path}")
try:
result = subprocess.run(
["git", "clone", ENGINE_REPO, str(self.engine_path)],
cwd=str(self.project_path),
creationflags=subprocess.CREATE_NO_WINDOW if platform.system() == "Windows" else 0
)
if result.returncode == 0:
print("✓ 引擎源码克隆成功!")
return True
else:
print("✗ 引擎源码克隆失败!")
return False
except Exception as e:
print(f"✗ 克隆过程中出错: {e}")
return False
def _create_main_cpp(self): def _create_main_cpp(self):
"""创建 main.cpp 文件(放在 src 目录下)""" """创建 main.cpp 文件(放在 src 目录下)"""
print("创建 src/main.cpp...") print("创建 src/main.cpp...")
@ -183,7 +337,7 @@ int main(int argc, char **argv) {{
f.write(content) f.write(content)
def _create_xmake_lua(self): def _create_xmake_lua(self):
"""创建 xmake.lua 文件(使用远程包引入方式""" """创建 xmake.lua 文件(使用项目内的引擎源码"""
print("创建 xmake.lua...") print("创建 xmake.lua...")
content = f'''-- ============================================== content = f'''-- ==============================================
-- {self.project_name} - Extra2D 游戏项目 -- {self.project_name} - Extra2D 游戏项目
@ -204,10 +358,96 @@ set_encodings("utf-8")
add_rules("mode.debug", "mode.release") add_rules("mode.debug", "mode.release")
-- ============================================== -- ==============================================
-- 依赖包通过 xmake 远程包管理 -- 平台检测与配置
-- ============================================== -- ==============================================
add_requires("extra2d") local host_plat = os.host()
local target_plat = get_config("plat") or host_plat
local supported_plats = {{mingw = true, switch = true}}
if not supported_plats[target_plat] then
if host_plat == "windows" then
target_plat = "mingw"
else
error("Unsupported platform: " .. target_plat .. ". Supported platforms: mingw, switch")
end
end
set_plat(target_plat)
if target_plat == "switch" then
set_arch("arm64")
elseif target_plat == "mingw" then
set_arch("x86_64")
end
-- ==============================================
-- 加载工具链配置
-- ==============================================
-- 引擎目录克隆的仓库目录
local engine_repo = "Extra2D"
-- 引擎源码目录仓库内的 Extra2D 子目录
local engine_src = path.join(engine_repo, "Extra2D")
if target_plat == "switch" then
includes(path.join(engine_repo, "xmake/toolchains/switch.lua"))
set_toolchains("switch")
elseif target_plat == "mingw" then
set_toolchains("mingw")
end
-- ==============================================
-- 添加依赖包 (MinGW)
-- ==============================================
if target_plat == "mingw" then
add_requires("glm", "libsdl2", "libsdl2_mixer")
end
-- ==============================================
-- 引擎库定义
-- ==============================================
target("extra2d")
set_kind("static")
-- 引擎源文件
add_files(path.join(engine_src, "src/**.cpp"))
add_files(path.join(engine_src, "src/glad/glad.c"))
-- 头文件路径
add_includedirs(path.join(engine_src, "include"), {{public = true}})
add_includedirs(path.join(engine_src, "include/extra2d/platform"), {{public = true}})
-- 平台配置
if target_plat == "switch" then
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
add_includedirs(devkitPro .. "/portlibs/switch/include", {{public = true}})
add_linkdirs(devkitPro .. "/portlibs/switch/lib")
add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", "vorbisidec", "ogg",
"modplug", "mpg123", "FLAC", "GLESv2", "EGL", "glapi", "drm_nouveau",
{{public = true}})
elseif target_plat == "mingw" then
add_packages("glm", "libsdl2", "libsdl2_mixer", {{public = true}})
add_syslinks("opengl32", "glu32", "winmm", "imm32", "version", "setupapi", {{public = true}})
end
-- 编译器标志
add_cxflags("-Wall", "-Wextra", {{force = true}})
add_cxflags("-Wno-unused-variable", "-Wno-unused-function", "-Wno-unused-parameter", {{force = true}})
add_cxflags("-Wno-strict-aliasing", "-Wno-implicit-fallthrough", {{force = true}})
add_cxflags("-Wno-missing-field-initializers", {{force = true}})
add_cxxflags("-Wno-deprecated-copy", "-Wno-class-memaccess", {{force = true}})
if is_mode("debug") then
add_defines("E2D_DEBUG", "_DEBUG", {{public = true}})
add_cxxflags("-O0", "-g", {{force = true}})
else
add_defines("NDEBUG", {{public = true}})
add_cxxflags("-O2", {{force = true}})
end
target_end()
-- ============================================== -- ==============================================
-- 目标定义 -- 目标定义
@ -216,7 +456,7 @@ add_requires("extra2d")
target("{self.project_name}") target("{self.project_name}")
set_kind("binary") set_kind("binary")
add_files("src/**.cpp") add_files("src/**.cpp")
add_packages("extra2d") add_deps("extra2d")
-- Nintendo Switch 平台配置 -- Nintendo Switch 平台配置
if is_plat("switch") then if is_plat("switch") then
@ -278,6 +518,18 @@ target("{self.project_name}")
end) end)
end end
target_end() target_end()
-- ==============================================
-- 项目信息输出
-- ==============================================
print("========================================")
print("{self.project_name} Build Configuration")
print("========================================")
print("Platform: " .. target_plat)
print("Architecture: " .. (get_config("arch") or "auto"))
print("Mode: " .. (is_mode("debug") and "debug" or "release"))
print("========================================")
''' '''
with open(self.project_path / "xmake.lua", "w", encoding="utf-8") as f: with open(self.project_path / "xmake.lua", "w", encoding="utf-8") as f:
f.write(content) f.write(content)
@ -320,6 +572,21 @@ Thumbs.db
- MinGW (Windows) - MinGW (Windows)
- Nintendo Switch - Nintendo Switch
## 项目结构
```
{self.project_name}/
src/ # 源代码目录
main.cpp # 主程序入口
romfs/ # 资源文件目录
assets/
images/ # 图片资源
audio/ # 音频资源
Extra2D/ # 引擎源码
xmake.lua # 构建配置
README.md # 项目说明
```
## 构建说明 ## 构建说明
### 前置要求 ### 前置要求
@ -354,20 +621,6 @@ xmake build
# 生成的 NRO 文件位于 build/switch/ 目录 # 生成的 NRO 文件位于 build/switch/ 目录
``` ```
### 项目结构
```
{self.project_name}/
src/ # 源代码目录
main.cpp # 主程序入口
romfs/ # 资源文件目录
assets/
images/ # 图片资源
audio/ # 音频资源
xmake.lua # 构建配置
README.md # 项目说明
```
## 资源文件 ## 资源文件
请将游戏所需的资源文件放入 `romfs/assets/` 目录 请将游戏所需的资源文件放入 `romfs/assets/` 目录
@ -387,6 +640,74 @@ MIT License
with open(self.project_path / "README.md", "w", encoding="utf-8") as f: with open(self.project_path / "README.md", "w", encoding="utf-8") as f:
f.write(content) f.write(content)
def _print_next_steps(self):
"""打印后续步骤"""
print(f"\n后续步骤:")
print(f" 1. cd {self.project_path}")
print(f" 2. xmake config -p mingw")
print(f" 3. xmake build")
print(f" 4. xmake run {self.project_name}")
def prompt_download_tools(dev_tools: dict):
"""
提示用户下载缺失的开发工具链
@param dev_tools: 缺失的开发工具链
"""
if not dev_tools:
return
print("\n========================================")
print("检测到以下开发工具链未安装:")
print("========================================")
if "mingw" in dev_tools:
print(" - MinGW-w64 (Windows 开发)")
if "devkitpro" in dev_tools:
print(" - devkitPro (Switch 开发)")
print("\n是否需要下载这些工具?")
print(" 1. 下载 MinGW-w64 (Windows 开发)")
print(" 2. 下载 devkitPro (Switch 开发)")
print(" 3. 下载全部")
print(" 4. 跳过下载")
try:
choice = input("\n请选择 (1-4): ").strip()
import webbrowser
if choice == "1" and "mingw" in dev_tools:
print(f"\n正在打开 MinGW 下载页面...")
print(f"下载地址: {MINGW_URL}")
webbrowser.open(MINGW_URL)
elif choice == "2" and "devkitpro" in dev_tools:
print(f"\n正在打开 devkitPro 下载页面...")
print(f"下载地址: {DEVKITPRO_URL}")
webbrowser.open(DEVKITPRO_URL)
elif choice == "3":
if "mingw" in dev_tools:
print(f"\n正在打开 MinGW 下载页面...")
print(f"下载地址: {MINGW_URL}")
webbrowser.open(MINGW_URL)
if "devkitpro" in dev_tools:
print(f"\n正在打开 devkitPro 下载页面...")
print(f"下载地址: {DEVKITPRO_URL}")
webbrowser.open(DEVKITPRO_URL)
else:
print("\n已跳过下载。")
if dev_tools:
print("\n安装提示:")
if "mingw" in dev_tools:
print(" MinGW: 解压后将 bin 目录添加到系统 PATH 环境变量")
if "devkitpro" in dev_tools:
print(" devkitPro: 运行安装程序,按提示完成安装")
except KeyboardInterrupt:
print("\n已取消。")
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
@ -397,6 +718,16 @@ def main():
python create_project.py my_game python create_project.py my_game
python create_project.py my_game --path ./games python create_project.py my_game --path ./games
python create_project.py my_game --author "Your Name" python create_project.py my_game --author "Your Name"
项目结构:
my_game/
src/
main.cpp
romfs/
assets/
xmake.lua
README.md
Extra2D/ # 引擎源码(自动克隆)
""" """
) )
@ -407,7 +738,7 @@ def main():
parser.add_argument( parser.add_argument(
"--path", "-p", "--path", "-p",
help="项目创建路径(默认为引擎根目录下的 projects 文件夹" help="项目创建路径(默认为当前目录"
) )
parser.add_argument( parser.add_argument(
@ -418,12 +749,25 @@ def main():
args = parser.parse_args() args = parser.parse_args()
# 验证项目名称 checker = DevToolsChecker()
checker.print_tool_status()
missing_tools = checker.get_missing_tools()
if missing_tools:
print("错误: 缺少必要的工具:")
for tool, desc in missing_tools.items():
print(f" - {desc}")
print("\n请先安装这些工具后再创建项目。")
sys.exit(1)
dev_tools = checker.get_missing_dev_tools()
if dev_tools:
prompt_download_tools(dev_tools)
if not args.name.replace("_", "").isalnum(): if not args.name.replace("_", "").isalnum():
print("错误: 项目名称只能包含字母、数字和下划线") print("错误: 项目名称只能包含字母、数字和下划线")
sys.exit(1) sys.exit(1)
# 创建项目
creator = ProjectCreator( creator = ProjectCreator(
project_name=args.name, project_name=args.name,
output_path=args.path, output_path=args.path,