rust_zw3d_build/
lib.rs

1use dirs::home_dir;
2use serde::Deserialize;
3use std::collections::HashSet;
4use std::env;
5use std::fmt;
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9use thiserror::Error;
10
11/// SDK 配置解析错误
12#[derive(Debug, Error)]
13pub enum ConfigError {
14    #[error("未找到 zw3d.toml,已检查: {0}")]
15    NotFound(String),
16    #[error("读取配置失败 {path}")]
17    Io {
18        path: PathBuf,
19        #[source]
20        source: std::io::Error,
21    },
22    #[error("解析配置失败 {path}")]
23    Parse {
24        path: PathBuf,
25        #[source]
26        source: toml::de::Error,
27    },
28    #[error("配置无效: {0}")]
29    Invalid(String),
30    #[error("环境变量未设置: {0}")]
31    MissingEnv(String),
32}
33
34/// 解析后的 SDK 配置
35#[derive(Debug, Clone)]
36pub struct SdkConfig {
37    pub lib_dir: PathBuf,
38    pub include_dir: PathBuf,
39    pub version: String,
40    pub config_path: Option<PathBuf>,
41}
42
43impl fmt::Display for SdkConfig {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(
46            f,
47            "version={}, lib_dir={}, include_dir={}",
48            self.version,
49            self.lib_dir.display(),
50            self.include_dir.display()
51        )
52    }
53}
54
55#[derive(Debug, Deserialize)]
56struct RawConfig {
57    zw3d: Zw3dSection,
58}
59
60#[derive(Debug, Deserialize)]
61struct Zw3dSection {
62    path: String,
63    #[serde(default)]
64    include: Option<String>,
65    #[serde(default)]
66    version: Option<String>,
67}
68
69/// 解析并校验 SDK 配置;失败时返回详细错误
70pub fn resolve_sdk() -> Result<SdkConfig, ConfigError> {
71    match find_config_path() {
72        Ok(path) => resolve_from_path(&path),
73        Err(ConfigError::NotFound(searched)) => match resolve_from_env() {
74            Ok(cfg) => Ok(cfg),
75            Err(ConfigError::MissingEnv(var)) => Err(ConfigError::NotFound(format!(
76                "{};未设置环境变量 {}",
77                searched, var
78            ))),
79            Err(other) => Err(other),
80        },
81        Err(err) => Err(err),
82    }
83}
84
85/// 解析配置并在失败时 panic,适用于 build.rs
86pub fn ensure_sdk() -> SdkConfig {
87    match resolve_sdk() {
88        Ok(cfg) => cfg,
89        Err(err) => panic!("无法解析 ZW3D SDK 配置: {}", err),
90    }
91}
92
93fn find_config_path() -> Result<PathBuf, ConfigError> {
94    let mut searched = Vec::new();
95    let mut seen = HashSet::new();
96
97    if let Ok(custom) = env::var("ZW3D_TOML") {
98        let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
99        let path = expand_path(PathBuf::from(custom), &cwd);
100        if path.is_file() {
101            return Ok(path);
102        }
103        searched.push(path.clone());
104        seen.insert(path);
105    }
106
107    let start_dir = env::var("CARGO_MANIFEST_DIR")
108        .map(PathBuf::from)
109        .ok()
110        .or_else(|| env::current_dir().ok())
111        .unwrap_or_else(|| PathBuf::from("."));
112
113    let mut current = start_dir.as_path();
114    loop {
115        let candidate = current.join("zw3d.toml");
116        if candidate.is_file() {
117            return Ok(candidate);
118        }
119        if seen.insert(candidate.clone()) {
120            searched.push(candidate);
121        }
122        if let Some(parent) = current.parent() {
123            current = parent;
124        } else {
125            break;
126        }
127    }
128
129    if let Ok(out_dir) = env::var("OUT_DIR") {
130        let mut current = PathBuf::from(out_dir);
131        while let Some(parent) = current.parent() {
132            current = parent.to_path_buf();
133            let candidate = current.join("zw3d.toml");
134            if candidate.is_file() {
135                return Ok(candidate);
136            }
137            if seen.insert(candidate.clone()) {
138                searched.push(candidate);
139            }
140        }
141    }
142
143    let joined = if searched.is_empty() {
144        format!(
145            "{}",
146            start_dir
147                .ancestors()
148                .map(|p| p.join("zw3d.toml").display().to_string())
149                .collect::<Vec<_>>()
150                .join(", ")
151        )
152    } else {
153        searched
154            .iter()
155            .map(|p| p.display().to_string())
156            .collect::<Vec<_>>()
157            .join(", ")
158    };
159
160    Err(ConfigError::NotFound(joined))
161}
162
163fn resolve_from_path(config_path: &Path) -> Result<SdkConfig, ConfigError> {
164    let contents = fs::read_to_string(config_path).map_err(|source| ConfigError::Io {
165        path: config_path.to_path_buf(),
166        source,
167    })?;
168
169    let raw: RawConfig = toml::from_str(&contents).map_err(|source| ConfigError::Parse {
170        path: config_path.to_path_buf(),
171        source,
172    })?;
173
174    let base_dir = config_path
175        .parent()
176        .map(Path::to_path_buf)
177        .unwrap_or_else(|| PathBuf::from("."));
178
179    build_sdk_config(raw, &base_dir, Some(config_path.to_path_buf()))
180}
181
182fn resolve_from_env() -> Result<SdkConfig, ConfigError> {
183    let dir = env::var("ZW3D_DIR").map_err(|_| ConfigError::MissingEnv("ZW3D_DIR".into()))?;
184    let lib_dir = PathBuf::from(dir);
185    let include_dir = env::var("ZW3D_INCLUDE").ok().map(PathBuf::from);
186    let version = env::var("ZW3D_VERSION").unwrap_or_else(|_| "26".into());
187
188    validate_and_build_config(lib_dir, include_dir, version, None)
189}
190
191fn build_sdk_config(
192    raw: RawConfig,
193    base_dir: &Path,
194    config_path: Option<PathBuf>,
195) -> Result<SdkConfig, ConfigError> {
196    let path_str = expand_env_vars(&raw.zw3d.path)?;
197    let lib_dir = expand_path(PathBuf::from(path_str), base_dir);
198
199    let include_dir = if let Some(include_raw) = raw.zw3d.include {
200        let include_str = expand_env_vars(&include_raw)?;
201        Some(expand_path(PathBuf::from(include_str), base_dir))
202    } else {
203        None
204    };
205
206    let version_raw = raw.zw3d.version.unwrap_or_else(|| "26".into());
207    let version = expand_env_vars(&version_raw)?;
208
209    validate_and_build_config(lib_dir, include_dir, version, config_path)
210}
211
212/// 验证并构建 SDK 配置
213fn validate_and_build_config(
214    lib_dir: PathBuf,
215    include_dir: Option<PathBuf>,
216    version: String,
217    config_path: Option<PathBuf>,
218) -> Result<SdkConfig, ConfigError> {
219    // 验证 ZW3D.lib 存在
220    let lib_file = lib_dir.join("ZW3D.lib");
221    if !lib_file.exists() {
222        return Err(ConfigError::Invalid(format!(
223            "在 `{}` 下未找到 ZW3D.lib",
224            lib_dir.display()
225        )));
226    }
227
228    // 确定 include 目录
229    let include_dir = if let Some(dir) = include_dir {
230        if !dir.exists() {
231            return Err(ConfigError::Invalid(format!(
232                "头文件目录不存在: {}",
233                dir.display()
234            )));
235        }
236        dir
237    } else {
238        // 默认使用 ${lib_dir}/api/inc
239        let candidate = lib_dir.join("api").join("inc");
240        if !candidate.exists() {
241            return Err(ConfigError::Invalid(format!(
242                "未设置 include 路径,且 `{}` 下找不到 api/inc",
243                lib_dir.display()
244            )));
245        }
246        candidate
247    };
248
249    Ok(SdkConfig {
250        lib_dir,
251        include_dir,
252        version,
253        config_path,
254    })
255}
256
257fn expand_env_vars(value: &str) -> Result<String, ConfigError> {
258    let mut result = String::new();
259    let mut chars = value.chars().peekable();
260
261    while let Some(ch) = chars.next() {
262        if ch == '$' && matches!(chars.peek(), Some('{')) {
263            chars.next(); // consume '{'
264            let mut key = String::new();
265            while let Some(&c) = chars.peek() {
266                chars.next();
267                if c == '}' {
268                    break;
269                }
270                key.push(c);
271            }
272
273            if key.is_empty() {
274                return Err(ConfigError::Invalid(format!(
275                    "环境变量占位符格式无效: {}",
276                    value
277                )));
278            }
279
280            let env_val = env::var(&key).map_err(|_| ConfigError::MissingEnv(key.clone()))?;
281            result.push_str(&env_val);
282        } else {
283            result.push(ch);
284        }
285    }
286
287    Ok(result)
288}
289
290/// 检查是否存在 ui/*.ui 文件,如果存在则调用 zrc.exe 工具编译它们
291/// 假设 zrc.exe 在 SDK 工具目录下
292pub fn build_ui_resources(sdk_config: &SdkConfig) -> Result<(), ConfigError> {
293    let manifest_dir = env::var("CARGO_MANIFEST_DIR")
294        .map(PathBuf::from)
295        .ok()
296        .or_else(|| env::current_dir().ok())
297        .unwrap_or_else(|| PathBuf::from("."));
298    
299    let ui_dir = manifest_dir.join("ui");
300    
301    // 检查是否存在 .ui 文件
302    if let Ok(entries) = fs::read_dir(&ui_dir) {
303        let ui_files: Vec<_> = entries
304            .filter_map(|entry| entry.ok())
305            .filter(|entry| {
306                entry.path().is_file() && 
307                entry.path().extension().map_or(false, |ext| ext == "ui")
308            })
309            .collect();
310        
311        if !ui_files.is_empty() {
312            // 获取目标目录(target/debug 或 target/release)
313            let target_dir: PathBuf = env::var("CARGO_TARGET_DIR")
314                .map(PathBuf::from)
315                .unwrap_or_else(|_| manifest_dir.join("target"));
316            
317            // 获取构建配置(debug 或 release)
318            let profile = env::var("PROFILE")
319                .unwrap_or_else(|_| "debug".to_string());
320            
321            // 获取目标名称(假设是 crate 名称)
322            let target_name = env::var("CARGO_PKG_NAME")
323                .map_err(|_| ConfigError::Invalid("CARGO_PKG_NAME not set".into()))?;
324            
325            // 构建输出目录
326            let out_dir = target_dir.join(profile);
327            
328            // 构建 zrc.exe 路径
329            let zrc_path = sdk_config.lib_dir
330                .parent()
331                .ok_or_else(|| ConfigError::Invalid(format!("无法获取 SDK 根目录: {}", sdk_config.lib_dir.display())))?
332                .join("tools")
333                .join("zrc")
334                .join("zrc.exe");
335            
336            if !zrc_path.exists() {
337                return Err(ConfigError::Invalid(format!("zrc.exe 不存在: {}", zrc_path.display())));
338            }
339            
340            // 构建输出文件路径
341            let output_file = out_dir.join(format!("{}.zrc", target_name));
342            
343            // 调用 zrc.exe 命令
344            let status = Command::new(zrc_path)
345                .arg("ui/")
346                .arg("-o")
347                .arg(&output_file)
348                .current_dir(&manifest_dir)
349                .status()
350                .map_err(|e| ConfigError::Invalid(format!("调用 zrc.exe 失败: {}", e)))?;
351            
352            if !status.success() {
353                return Err(ConfigError::Invalid(format!("zrc.exe 执行失败,退出代码: {:?}", status.code())));
354            }
355            
356            // 告诉 cargo 当 ui 文件变化时重新构建
357            for ui_file in ui_files {
358                println!("cargo:rerun-if-changed={}", ui_file.path().display());
359            }
360            
361            // 告诉 cargo 输出文件位置
362            println!("cargo:rustc-env=ZW3D_UI_RESOURCE={}", output_file.display());
363        }
364    }
365    
366    Ok(())
367}
368
369fn expand_path(path: PathBuf, base: &Path) -> PathBuf {
370    if let Some(str_path) = path.to_str() {
371        if let Some(stripped) = str_path.strip_prefix('~') {
372            if let Some(home) = home_dir() {
373                let rest = stripped.strip_prefix(['/', '\\']).unwrap_or(stripped);
374                return home.join(rest);
375            }
376        }
377    }
378
379    if path.is_absolute() {
380        path
381    } else {
382        base.join(path)
383    }
384}
385
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use serial_test::serial;
391    use std::io::Write;
392
393    #[test]
394    #[serial]
395    fn parse_valid_config_with_env_expansion() {
396        // 清理所有可能的环境变量污染
397        unsafe {
398            env::remove_var("ZW3D_TOML");
399            env::remove_var("ZW3D_DIR");
400            env::remove_var("ZW3D_INCLUDE");
401            env::remove_var("ZW3D_VERSION");
402        }
403
404        let dir = tempfile::tempdir().unwrap();
405        let lib_dir = dir.path().join("lib");
406        let include_dir = dir.path().join("include");
407        fs::create_dir_all(&lib_dir).unwrap();
408        fs::create_dir_all(&include_dir).unwrap();
409        fs::write(lib_dir.join("ZW3D.lib"), b"").unwrap();
410
411        let config_path = dir.path().join("zw3d.toml");
412        let mut file = fs::File::create(&config_path).unwrap();
413
414        // 环境变量中直接设置路径,不需要转义
415        unsafe {
416            env::set_var("ZW3D_DIR", &lib_dir);
417            env::set_var("ZW3D_INCLUDE", &include_dir);
418            env::set_var("ZW3D_VERSION", "26");
419        }
420
421        write!(
422            file,
423            "[zw3d]\npath = \"${{ZW3D_DIR}}\"\ninclude = \"${{ZW3D_INCLUDE}}\"\nversion = \"${{ZW3D_VERSION}}\"\n",
424        )
425        .unwrap();
426
427        unsafe {
428            env::set_var("ZW3D_TOML", config_path.to_string_lossy().to_string());
429        }
430        let resolved = resolve_sdk().unwrap();
431
432        assert_eq!(resolved.lib_dir, lib_dir);
433        assert_eq!(resolved.include_dir, include_dir);
434        assert_eq!(resolved.version, "26");
435        assert_eq!(resolved.config_path.as_ref().unwrap(), &config_path);
436
437        unsafe {
438            env::remove_var("ZW3D_TOML");
439            env::remove_var("ZW3D_DIR");
440            env::remove_var("ZW3D_INCLUDE");
441            env::remove_var("ZW3D_VERSION");
442        }
443    }
444
445    #[test]
446    #[serial]
447    fn resolve_from_environment_only() {
448        // 清理所有可能的环境变量污染
449        unsafe {
450            env::remove_var("ZW3D_TOML");
451            env::remove_var("ZW3D_DIR");
452            env::remove_var("ZW3D_INCLUDE");
453            env::remove_var("ZW3D_VERSION");
454        }
455
456        let dir = tempfile::tempdir().unwrap();
457        let lib_dir = dir.path().join("lib");
458        let include_dir = lib_dir.join("api").join("inc");
459        fs::create_dir_all(&lib_dir).unwrap();
460        fs::create_dir_all(&include_dir).unwrap();
461        fs::write(lib_dir.join("ZW3D.lib"), b"").unwrap();
462
463        unsafe {
464            // ZW3D_DIR 直接指向包含 ZW3D.lib 的目录
465            env::set_var("ZW3D_DIR", &lib_dir);
466            env::set_var("ZW3D_VERSION", "25");
467        }
468
469        let cfg = resolve_from_env().unwrap();
470        assert_eq!(cfg.lib_dir, lib_dir);
471        assert_eq!(cfg.include_dir, include_dir);
472        assert_eq!(cfg.version, "25");
473        assert!(cfg.config_path.is_none());
474
475        unsafe {
476            env::remove_var("ZW3D_DIR");
477            env::remove_var("ZW3D_VERSION");
478        }
479    }
480}