1use std::fs;
2use std::path::{Component, Path, PathBuf};
3
4use crate::error::SboxError;
5
6use super::model::Config;
7use super::validate::validate_config;
8
9#[derive(Debug, Clone)]
10pub struct LoadOptions {
11 pub workspace: Option<PathBuf>,
12 pub config: Option<PathBuf>,
13}
14
15#[derive(Debug, Clone)]
16pub struct LoadedConfig {
17 pub invocation_dir: PathBuf,
18 pub workspace_root: PathBuf,
19 pub config_path: PathBuf,
20 pub config: Config,
21}
22
23pub fn load_config(options: &LoadOptions) -> Result<LoadedConfig, SboxError> {
24 let invocation_dir =
25 std::env::current_dir().map_err(|source| SboxError::CurrentDirectory { source })?;
26
27 let config_path = match options.config.as_deref() {
28 Some(path) => absolutize_from(path, &invocation_dir),
29 None => {
30 let search_root = absolutize_path(options.workspace.as_deref(), &invocation_dir)
31 .unwrap_or_else(|| invocation_dir.clone());
32 find_default_config_path(&search_root)?
33 }
34 };
35
36 if !config_path.exists() {
37 return Err(SboxError::ConfigNotFound(config_path));
38 }
39
40 let raw = fs::read_to_string(&config_path).map_err(|source| SboxError::ConfigRead {
41 path: config_path.clone(),
42 source,
43 })?;
44
45 let mut config = serde_yaml::from_str::<Config>(&raw).map_err(|source| SboxError::ConfigParse {
46 path: config_path.clone(),
47 source,
48 })?;
49
50 super::package_manager::elaborate(&mut config)?;
51 validate_config(&config)?;
52
53 let config_dir = config_path
54 .parent()
55 .map(Path::to_path_buf)
56 .unwrap_or_else(|| invocation_dir.clone());
57
58 let workspace_root = match options.workspace.as_deref() {
59 Some(path) => absolutize_from(path, &invocation_dir),
60 None => {
61 let configured_root = config
62 .workspace
63 .as_ref()
64 .and_then(|workspace| workspace.root.as_deref());
65
66 absolutize_path(configured_root, &config_dir).unwrap_or(config_dir)
67 }
68 };
69
70 Ok(LoadedConfig {
71 invocation_dir,
72 workspace_root,
73 config_path,
74 config,
75 })
76}
77
78fn absolutize_path(path: Option<&Path>, base: &Path) -> Option<PathBuf> {
79 path.map(|path| absolutize_from(path, base))
80}
81
82fn absolutize_from(path: &Path, base: &Path) -> PathBuf {
83 let combined = if path.is_absolute() {
84 path.to_path_buf()
85 } else {
86 base.join(path)
87 };
88
89 normalize_path(&combined)
90}
91
92fn find_default_config_path(start: &Path) -> Result<PathBuf, SboxError> {
93 for directory in start.ancestors() {
94 let candidate = directory.join("sbox.yaml");
95 if candidate.exists() {
96 return Ok(candidate);
97 }
98 }
99
100 Err(SboxError::ConfigNotFound(start.join("sbox.yaml")))
101}
102
103fn normalize_path(path: &Path) -> PathBuf {
104 let mut normalized = PathBuf::new();
105
106 for component in path.components() {
107 match component {
108 Component::CurDir => {}
109 Component::ParentDir => {
110 normalized.pop();
111 }
112 other => normalized.push(other.as_os_str()),
113 }
114 }
115
116 if normalized.as_os_str().is_empty() {
117 PathBuf::from(".")
118 } else {
119 normalized
120 }
121}