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#[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#[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
69pub 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
85pub 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
212fn 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 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 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 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(); 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
290pub 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 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 let target_dir: PathBuf = env::var("CARGO_TARGET_DIR")
314 .map(PathBuf::from)
315 .unwrap_or_else(|_| manifest_dir.join("target"));
316
317 let profile = env::var("PROFILE")
319 .unwrap_or_else(|_| "debug".to_string());
320
321 let target_name = env::var("CARGO_PKG_NAME")
323 .map_err(|_| ConfigError::Invalid("CARGO_PKG_NAME not set".into()))?;
324
325 let out_dir = target_dir.join(profile);
327
328 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 let output_file = out_dir.join(format!("{}.zrc", target_name));
342
343 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 for ui_file in ui_files {
358 println!("cargo:rerun-if-changed={}", ui_file.path().display());
359 }
360
361 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 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 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 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 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}