1use std::fs;
2use std::path::{Component, Path, PathBuf};
3
4use crate::error::SboxError;
5
6use super::model::Config;
7use super::validate::{emit_config_warnings, 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 =
46 serde_yaml::from_str::<Config>(&raw).map_err(|source| SboxError::ConfigParse {
47 path: config_path.clone(),
48 source,
49 })?;
50
51 super::package_manager::elaborate(&mut config)?;
52 validate_config(&config)?;
53 emit_config_warnings(&config);
54
55 let config_dir = config_path
56 .parent()
57 .map(Path::to_path_buf)
58 .unwrap_or_else(|| invocation_dir.clone());
59
60 let workspace_root = match options.workspace.as_deref() {
61 Some(path) => absolutize_from(path, &invocation_dir),
62 None => {
63 let configured_root = config
64 .workspace
65 .as_ref()
66 .and_then(|workspace| workspace.root.as_deref());
67
68 absolutize_path(configured_root, &config_dir).unwrap_or(config_dir)
69 }
70 };
71
72 Ok(LoadedConfig {
73 invocation_dir,
74 workspace_root,
75 config_path,
76 config,
77 })
78}
79
80fn absolutize_path(path: Option<&Path>, base: &Path) -> Option<PathBuf> {
81 path.map(|path| absolutize_from(path, base))
82}
83
84fn absolutize_from(path: &Path, base: &Path) -> PathBuf {
85 let combined = if path.is_absolute() {
86 path.to_path_buf()
87 } else {
88 base.join(path)
89 };
90
91 normalize_path(&combined)
92}
93
94fn find_default_config_path(start: &Path) -> Result<PathBuf, SboxError> {
95 for directory in start.ancestors() {
96 let candidate = directory.join("sbox.yaml");
97 if candidate.exists() {
98 return Ok(candidate);
99 }
100 }
101
102 if let Some(home) = std::env::var_os("HOME") {
105 let global = PathBuf::from(home).join(".config/sbox/sbox.yaml");
106 if global.exists() {
107 return Ok(global);
108 }
109 }
110
111 Err(SboxError::ConfigNotFound(start.join("sbox.yaml")))
112}
113
114fn normalize_path(path: &Path) -> PathBuf {
115 let mut normalized = PathBuf::new();
116
117 for component in path.components() {
118 match component {
119 Component::CurDir => {}
120 Component::ParentDir => {
121 normalized.pop();
122 }
123 other => normalized.push(other.as_os_str()),
124 }
125 }
126
127 if normalized.as_os_str().is_empty() {
128 PathBuf::from(".")
129 } else {
130 normalized
131 }
132}