Files
esengine/packages/editor-app/src-tauri/src/commands/build.rs

456 lines
14 KiB
Rust
Raw Normal View History

feat: 添加跨平台运行时、资产系统和UI适配功能 (#256) * feat(platform-common): 添加WASM加载器和环境检测API * feat(rapier2d): 新增Rapier2D WASM绑定包 * feat(physics-rapier2d): 添加跨平台WASM加载器 * feat(asset-system): 添加运行时资产目录和bundle格式 * feat(asset-system-editor): 新增编辑器资产管理包 * feat(editor-core): 添加构建系统和模块管理 * feat(editor-app): 重构浏览器预览使用import maps * feat(platform-web): 添加BrowserRuntime和资产读取 * feat(engine): 添加材质系统和着色器管理 * feat(material): 新增材质系统和着色器编辑器 * feat(tilemap): 增强tilemap编辑器和动画系统 * feat(modules): 添加module.json配置 * feat(core): 添加module.json和类型定义更新 * chore: 更新依赖和构建配置 * refactor(plugins): 更新插件模板使用ModuleManifest * chore: 添加第三方依赖库 * chore: 移除BehaviourTree-ai和ecs-astar子模块 * docs: 更新README和文档主题样式 * fix: 修复Rust文档测试和添加rapier2d WASM绑定 * fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题 * feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea) * fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖 * fix: 添加缺失的包依赖修复CI构建 * fix: 修复CodeQL检测到的代码问题 * fix: 修复构建错误和缺失依赖 * fix: 修复类型检查错误 * fix(material-system): 修复tsconfig配置支持TypeScript项目引用 * fix(editor-core): 修复Rollup构建配置添加tauri external * fix: 修复CodeQL检测到的代码问题 * fix: 修复CodeQL检测到的代码问题
2025-12-03 22:15:22 +08:00
//! Build related commands.
//! 构建相关命令。
//!
//! Provides file operations and compilation for build pipelines.
//! 为构建管线提供文件操作和编译功能。
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use std::process::Command;
/// Build progress event.
/// 构建进度事件。
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildProgressEvent {
/// Progress percentage (0-100) | 进度百分比
pub progress: u32,
/// Current step message | 当前步骤消息
pub message: String,
/// Current step index | 当前步骤索引
pub current_step: u32,
/// Total steps | 总步骤数
pub total_steps: u32,
}
/// Clean and recreate output directory.
/// 清理并重建输出目录。
#[tauri::command]
pub async fn prepare_build_directory(output_path: String) -> Result<(), String> {
let path = Path::new(&output_path);
// Remove existing directory if exists | 如果存在则删除现有目录
if path.exists() {
fs::remove_dir_all(path)
.map_err(|e| format!("Failed to clean output directory | 清理输出目录失败: {}", e))?;
}
// Create fresh directory | 创建新目录
fs::create_dir_all(path)
.map_err(|e| format!("Failed to create output directory | 创建输出目录失败: {}", e))?;
Ok(())
}
/// Copy directory recursively.
/// 递归复制目录。
#[tauri::command]
pub async fn copy_directory(
src: String,
dst: String,
patterns: Option<Vec<String>>,
) -> Result<u32, String> {
let src_path = Path::new(&src);
let dst_path = Path::new(&dst);
if !src_path.exists() {
return Err(format!("Source directory does not exist | 源目录不存在: {}", src));
}
// Create destination directory | 创建目标目录
fs::create_dir_all(dst_path)
.map_err(|e| format!("Failed to create destination directory | 创建目标目录失败: {}", e))?;
let mut copied_count = 0u32;
// Recursively copy | 递归复制
copy_dir_recursive(src_path, dst_path, &patterns, &mut copied_count)?;
Ok(copied_count)
}
/// Helper function to copy directory recursively.
/// 递归复制目录的辅助函数。
fn copy_dir_recursive(
src: &Path,
dst: &Path,
patterns: &Option<Vec<String>>,
count: &mut u32,
) -> Result<(), String> {
for entry in fs::read_dir(src)
.map_err(|e| format!("Failed to read directory | 读取目录失败: {}", e))?
{
let entry = entry.map_err(|e| format!("Failed to read entry | 读取条目失败: {}", e))?;
let src_path = entry.path();
let file_name = entry.file_name();
let dst_path = dst.join(&file_name);
if src_path.is_dir() {
// Skip hidden directories | 跳过隐藏目录
if file_name.to_string_lossy().starts_with('.') {
continue;
}
fs::create_dir_all(&dst_path)
.map_err(|e| format!("Failed to create directory | 创建目录失败: {}", e))?;
copy_dir_recursive(&src_path, &dst_path, patterns, count)?;
} else {
// Check if file matches patterns | 检查文件是否匹配模式
if let Some(ref pats) = patterns {
let file_name_str = file_name.to_string_lossy();
let matches = pats.iter().any(|p| {
if p.starts_with("*.") {
let ext = &p[2..];
file_name_str.ends_with(&format!(".{}", ext))
} else {
file_name_str.contains(p)
}
});
if !matches {
continue;
}
}
fs::copy(&src_path, &dst_path)
.map_err(|e| format!("Failed to copy file | 复制文件失败: {} -> {}: {}",
src_path.display(), dst_path.display(), e))?;
*count += 1;
}
}
Ok(())
}
/// Bundle options for esbuild.
/// esbuild 打包选项。
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BundleOptions {
/// Entry files | 入口文件
pub entry_points: Vec<String>,
/// Output directory | 输出目录
pub output_dir: String,
/// Output format (esm or iife) | 输出格式
pub format: String,
/// Bundle name | 打包名称
pub bundle_name: String,
/// Whether to minify | 是否压缩
pub minify: bool,
/// Whether to generate source map | 是否生成 source map
pub source_map: bool,
/// External dependencies | 外部依赖
pub external: Vec<String>,
/// Project root for resolving imports | 项目根目录
pub project_root: String,
/// Define replacements | 宏定义替换
pub define: Option<std::collections::HashMap<String, String>>,
}
/// Bundle result.
/// 打包结果。
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BundleResult {
/// Whether bundling succeeded | 是否打包成功
pub success: bool,
/// Output file path | 输出文件路径
pub output_file: Option<String>,
/// Output file size in bytes | 输出文件大小(字节)
pub output_size: Option<u64>,
/// Error message if failed | 失败时的错误信息
pub error: Option<String>,
/// Warnings | 警告
pub warnings: Vec<String>,
}
/// Bundle JavaScript/TypeScript files using esbuild.
/// 使用 esbuild 打包 JavaScript/TypeScript 文件。
#[tauri::command]
pub async fn bundle_scripts(options: BundleOptions) -> Result<BundleResult, String> {
let esbuild_path = find_esbuild(&options.project_root)?;
// Build output file path | 构建输出文件路径
let output_file = Path::new(&options.output_dir)
.join(&options.bundle_name)
.with_extension("js");
// Ensure output directory exists | 确保输出目录存在
if let Some(parent) = output_file.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create output directory | 创建输出目录失败: {}", e))?;
}
// Build esbuild arguments | 构建 esbuild 参数
let mut args: Vec<String> = options.entry_points.clone();
args.push("--bundle".to_string());
args.push(format!("--outfile={}", output_file.display()));
args.push(format!("--format={}", options.format));
args.push("--platform=browser".to_string());
args.push("--target=es2020".to_string());
if options.source_map {
args.push("--sourcemap".to_string());
}
if options.minify {
args.push("--minify".to_string());
}
for external in &options.external {
args.push(format!("--external:{}", external));
}
// Add define replacements | 添加宏定义替换
if let Some(ref defines) = options.define {
for (key, value) in defines {
args.push(format!("--define:{}={}", key, value));
}
}
// Run esbuild | 运行 esbuild
let output = Command::new(&esbuild_path)
.args(&args)
.current_dir(&options.project_root)
.output()
.map_err(|e| format!("Failed to run esbuild | 运行 esbuild 失败: {}", e))?;
if output.status.success() {
// Get output file size | 获取输出文件大小
let output_size = fs::metadata(&output_file)
.map(|m| m.len())
.ok();
// Parse warnings from stderr | 从 stderr 解析警告
let stderr = String::from_utf8_lossy(&output.stderr);
let warnings: Vec<String> = stderr
.lines()
.filter(|l| l.contains("warning"))
.map(|l| l.to_string())
.collect();
Ok(BundleResult {
success: true,
output_file: Some(output_file.to_string_lossy().to_string()),
output_size,
error: None,
warnings,
})
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Ok(BundleResult {
success: false,
output_file: None,
output_size: None,
error: Some(stderr.to_string()),
warnings: vec![],
})
}
}
/// Generate HTML file from template.
/// 从模板生成 HTML 文件。
#[tauri::command]
pub async fn generate_html(
output_path: String,
title: String,
scripts: Vec<String>,
body_content: Option<String>,
) -> Result<(), String> {
let scripts_html: String = scripts
.iter()
.map(|s| format!(r#" <script src="{}"></script>"#, s))
.collect::<Vec<_>>()
.join("\n");
let body = body_content.unwrap_or_else(|| {
r#" <canvas id="game-canvas" style="width: 100%; height: 100%;"></canvas>"#.to_string()
});
let html = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{}</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
html, body {{ width: 100%; height: 100%; overflow: hidden; background: #000; }}
</style>
</head>
<body>
{}
{}
</body>
</html>"#,
title, body, scripts_html
);
// Ensure parent directory exists | 确保父目录存在
let path = Path::new(&output_path);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create directory | 创建目录失败: {}", e))?;
}
fs::write(&output_path, html)
.map_err(|e| format!("Failed to write HTML file | 写入 HTML 文件失败: {}", e))?;
Ok(())
}
/// Get file size.
/// 获取文件大小。
#[tauri::command]
pub async fn get_file_size(file_path: String) -> Result<u64, String> {
fs::metadata(&file_path)
.map(|m| m.len())
.map_err(|e| format!("Failed to get file size | 获取文件大小失败: {}", e))
}
/// Get directory size recursively.
/// 递归获取目录大小。
#[tauri::command]
pub async fn get_directory_size(dir_path: String) -> Result<u64, String> {
let path = Path::new(&dir_path);
if !path.exists() {
return Err(format!("Directory does not exist | 目录不存在: {}", dir_path));
}
calculate_dir_size(path)
}
/// Helper to calculate directory size.
/// 计算目录大小的辅助函数。
fn calculate_dir_size(path: &Path) -> Result<u64, String> {
let mut total_size = 0u64;
for entry in fs::read_dir(path)
.map_err(|e| format!("Failed to read directory | 读取目录失败: {}", e))?
{
let entry = entry.map_err(|e| format!("Failed to read entry | 读取条目失败: {}", e))?;
let entry_path = entry.path();
if entry_path.is_dir() {
total_size += calculate_dir_size(&entry_path)?;
} else {
total_size += fs::metadata(&entry_path)
.map(|m| m.len())
.unwrap_or(0);
}
}
Ok(total_size)
}
/// Find esbuild executable.
/// 查找 esbuild 可执行文件。
fn find_esbuild(project_root: &str) -> Result<String, String> {
let project_path = Path::new(project_root);
// Try local node_modules first | 首先尝试本地 node_modules
let local_esbuild = if cfg!(windows) {
project_path.join("node_modules/.bin/esbuild.cmd")
} else {
project_path.join("node_modules/.bin/esbuild")
};
if local_esbuild.exists() {
return Ok(local_esbuild.to_string_lossy().to_string());
}
// Try global esbuild | 尝试全局 esbuild
let global_esbuild = if cfg!(windows) { "esbuild.cmd" } else { "esbuild" };
let check = Command::new(global_esbuild)
.arg("--version")
.output();
match check {
Ok(output) if output.status.success() => Ok(global_esbuild.to_string()),
_ => Err("esbuild not found | 未找到 esbuild".to_string())
}
}
/// Write JSON file.
/// 写入 JSON 文件。
#[tauri::command]
pub async fn write_json_file(file_path: String, content: String) -> Result<(), String> {
let path = Path::new(&file_path);
// Ensure parent directory exists | 确保父目录存在
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create directory | 创建目录失败: {}", e))?;
}
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write JSON file | 写入 JSON 文件失败: {}", e))?;
Ok(())
}
/// List files in directory with extension filter.
/// 列出目录中指定扩展名的文件。
#[tauri::command]
pub async fn list_files_by_extension(
dir_path: String,
extensions: Vec<String>,
recursive: bool,
) -> Result<Vec<String>, String> {
let path = Path::new(&dir_path);
if !path.exists() {
return Ok(vec![]);
}
let mut files = Vec::new();
list_files_recursive(path, &extensions, recursive, &mut files)?;
Ok(files)
}
/// Helper to list files recursively.
/// 递归列出文件的辅助函数。
fn list_files_recursive(
path: &Path,
extensions: &[String],
recursive: bool,
files: &mut Vec<String>,
) -> Result<(), String> {
for entry in fs::read_dir(path)
.map_err(|e| format!("Failed to read directory | 读取目录失败: {}", e))?
{
let entry = entry.map_err(|e| format!("Failed to read entry | 读取条目失败: {}", e))?;
let entry_path = entry.path();
if entry_path.is_dir() {
if recursive {
list_files_recursive(&entry_path, extensions, recursive, files)?;
}
} else if let Some(ext) = entry_path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
if extensions.iter().any(|e| e.to_lowercase() == ext_str) {
files.push(entry_path.to_string_lossy().to_string());
}
}
}
Ok(())
}
/// Read binary file and return as base64.
/// 读取二进制文件并返回 base64 编码。
#[tauri::command]
pub async fn read_binary_file_as_base64(path: String) -> Result<String, String> {
use base64::{Engine as _, engine::general_purpose::STANDARD};
let bytes = fs::read(&path)
.map_err(|e| format!("Failed to read binary file | 读取二进制文件失败: {}", e))?;
Ok(STANDARD.encode(&bytes))
}