1use wowlab_fs::{
4 directory::{self, EntryKind},
5 path::{Path, PathBuf},
6 working_directory,
7};
8
9use super::output;
10
11trait WorkspaceEnvironment {
12 fn var(&self, key: &str) -> Option<String>;
13 fn current_dir(&self) -> Option<PathBuf>;
14}
15
16struct ProcessWorkspaceEnvironment;
17
18impl WorkspaceEnvironment for ProcessWorkspaceEnvironment {
19 fn var(&self, key: &str) -> Option<String> {
20 std::env::var(key).ok()
22 }
23
24 fn current_dir(&self) -> Option<PathBuf> {
25 working_directory::current().ok()
26 }
27}
28
29#[derive(Debug)]
31pub struct CliApp {
32 pub root: PathBuf,
33 pub quiet: bool,
34}
35
36impl CliApp {
37 #[must_use]
38 pub fn crates_dir(&self) -> PathBuf {
39 self.root.join("crates")
40 }
41}
42
43#[must_use]
45pub fn boot(name: &str, version: &str, quiet: bool, root_env_var: &str) -> CliApp {
46 if !quiet {
47 output::banner(name, version);
48 }
49
50 let root = workspace_root(root_env_var);
51
52 CliApp { root, quiet }
53}
54
55#[must_use]
61pub fn workspace_root(env_var: &str) -> PathBuf {
62 workspace_root_with(&ProcessWorkspaceEnvironment, env_var)
63 .expect("cannot determine working directory")
64}
65
66fn workspace_root_with(env: &impl WorkspaceEnvironment, env_var: &str) -> Option<PathBuf> {
67 if let Some(dir) = env.var(env_var) {
68 let root = PathBuf::from(dir);
69
70 if is_directory(&root.join("crates")) {
71 return Some(root);
72 }
73 }
74
75 if let Some(dir) = env.var("CARGO_MANIFEST_DIR") {
76 if let Some(root) = PathBuf::from(dir)
77 .parent()
78 .and_then(|p| p.parent())
79 .filter(|root| is_directory(&root.join("crates")))
80 {
81 return Some(root.to_path_buf());
82 }
83 }
84
85 let current_dir = env.current_dir()?;
86 let mut candidate = current_dir.clone();
87
88 loop {
89 if entry_exists(&candidate.join("Cargo.toml")) && is_directory(&candidate.join("crates")) {
90 return Some(candidate);
91 }
92
93 if !candidate.pop() {
94 break;
95 }
96 }
97
98 Some(current_dir)
99}
100
101fn entry_exists(path: &Path) -> bool {
102 directory::inspect(path).is_ok_and(|entry| entry.is_some())
103}
104
105fn is_directory(path: &Path) -> bool {
106 directory::inspect(path)
107 .is_ok_and(|entry| entry.is_some_and(|entry| entry.kind() == EntryKind::Directory))
108}
109
110#[cfg(test)]
111mod tests {
112 use googletest::prelude::*;
113
114 use super::*;
115
116 struct TestEnvironment {
117 explicit_root: Option<PathBuf>,
118 manifest_dir: Option<PathBuf>,
119 current_dir: PathBuf,
120 }
121
122 impl WorkspaceEnvironment for TestEnvironment {
123 fn var(&self, key: &str) -> Option<String> {
124 match key {
125 "TEST_ROOT" => self
126 .explicit_root
127 .as_ref()
128 .map(|path| path.display().to_string()),
129 "CARGO_MANIFEST_DIR" => self
130 .manifest_dir
131 .as_ref()
132 .map(|path| path.display().to_string()),
133 _ => None,
134 }
135 }
136
137 fn current_dir(&self) -> Option<PathBuf> {
138 Some(self.current_dir.clone())
139 }
140 }
141
142 fn workspace(directory: &wowlab_fs::temporary::Directory, name: &str) -> Result<PathBuf> {
143 let root = directory.path().join(name);
144
145 directory::ensure(&root.join("crates")).or_fail()?;
146 wowlab_fs::file::write_text(&root.join("Cargo.toml"), "[workspace]\n").or_fail()?;
147
148 Ok(root)
149 }
150
151 #[gtest]
152 fn explicit_root_has_precedence() -> Result<()> {
153 let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
154 let explicit_root = workspace(&directory, "explicit").or_fail()?;
155 let manifest_root = workspace(&directory, "manifest").or_fail()?;
156 let current_root = workspace(&directory, "current").or_fail()?;
157 let environment = TestEnvironment {
158 explicit_root: Some(explicit_root.clone()),
159 manifest_dir: Some(manifest_root.join("crates/common")),
160 current_dir: current_root,
161 };
162
163 verify_that!(
164 workspace_root_with(&environment, "TEST_ROOT").as_deref(),
165 some(eq(&*explicit_root))
166 )
167 }
168
169 #[gtest]
170 fn manifest_directory_is_used_when_explicit_root_is_invalid() -> Result<()> {
171 let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
172 let manifest_root = workspace(&directory, "manifest").or_fail()?;
173 let current_root = workspace(&directory, "current").or_fail()?;
174 let environment = TestEnvironment {
175 explicit_root: Some(directory.path().join("missing")),
176 manifest_dir: Some(manifest_root.join("crates/common")),
177 current_dir: current_root,
178 };
179
180 verify_that!(
181 workspace_root_with(&environment, "TEST_ROOT").as_deref(),
182 some(eq(&*manifest_root))
183 )
184 }
185
186 #[gtest]
187 fn current_directory_walks_to_workspace_ancestor() -> Result<()> {
188 let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
189 let root = workspace(&directory, "current").or_fail()?;
190 let nested = root.join("crates/common/src");
191
192 directory::ensure(&nested).or_fail()?;
193 let environment = TestEnvironment {
194 explicit_root: None,
195 manifest_dir: None,
196 current_dir: nested,
197 };
198
199 verify_that!(
200 workspace_root_with(&environment, "TEST_ROOT").as_deref(),
201 some(eq(&*root))
202 )
203 }
204
205 #[gtest]
206 fn current_directory_is_the_fallback_without_workspace_markers() -> Result<()> {
207 let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
208 let current_dir = directory.path().join("plain/directory");
209
210 directory::ensure(¤t_dir).or_fail()?;
211 let environment = TestEnvironment {
212 explicit_root: None,
213 manifest_dir: None,
214 current_dir: current_dir.clone(),
215 };
216
217 verify_that!(
218 workspace_root_with(&environment, "TEST_ROOT").as_deref(),
219 some(eq(&*current_dir))
220 )
221 }
222}