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 && 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 let current_dir = env.current_dir()?;
85 let mut candidate = current_dir.clone();
86
87 loop {
88 if entry_exists(&candidate.join("Cargo.toml")) && is_directory(&candidate.join("crates")) {
89 return Some(candidate);
90 }
91
92 if !candidate.pop() {
93 break;
94 }
95 }
96
97 Some(current_dir)
98}
99
100fn entry_exists(path: &Path) -> bool {
101 directory::inspect(path).is_ok_and(|entry| entry.is_some())
102}
103
104fn is_directory(path: &Path) -> bool {
105 directory::inspect(path)
106 .is_ok_and(|entry| entry.is_some_and(|entry| entry.kind() == EntryKind::Directory))
107}
108
109#[cfg(test)]
110mod tests {
111 use googletest::prelude::*;
112
113 use super::*;
114
115 struct TestEnvironment {
116 explicit_root: Option<PathBuf>,
117 manifest_dir: Option<PathBuf>,
118 current_dir: PathBuf,
119 }
120
121 impl WorkspaceEnvironment for TestEnvironment {
122 fn var(&self, key: &str) -> Option<String> {
123 match key {
124 "TEST_ROOT" => self
125 .explicit_root
126 .as_ref()
127 .map(|path| path.display().to_string()),
128 "CARGO_MANIFEST_DIR" => self
129 .manifest_dir
130 .as_ref()
131 .map(|path| path.display().to_string()),
132 _ => None,
133 }
134 }
135
136 fn current_dir(&self) -> Option<PathBuf> {
137 Some(self.current_dir.clone())
138 }
139 }
140
141 fn workspace(directory: &wowlab_fs::temporary::Directory, name: &str) -> Result<PathBuf> {
142 let root = directory.path().join(name);
143
144 directory::ensure(&root.join("crates")).or_fail()?;
145 wowlab_fs::file::write_text(&root.join("Cargo.toml"), "[workspace]\n").or_fail()?;
146
147 Ok(root)
148 }
149
150 #[gtest]
151 fn explicit_root_has_precedence() -> Result<()> {
152 let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
153 let explicit_root = workspace(&directory, "explicit").or_fail()?;
154 let manifest_root = workspace(&directory, "manifest").or_fail()?;
155 let current_root = workspace(&directory, "current").or_fail()?;
156 let environment = TestEnvironment {
157 explicit_root: Some(explicit_root.clone()),
158 manifest_dir: Some(manifest_root.join("crates/common")),
159 current_dir: current_root,
160 };
161
162 verify_that!(
163 workspace_root_with(&environment, "TEST_ROOT").as_deref(),
164 some(eq(&*explicit_root))
165 )
166 }
167
168 #[gtest]
169 fn manifest_directory_is_used_when_explicit_root_is_invalid() -> Result<()> {
170 let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
171 let manifest_root = workspace(&directory, "manifest").or_fail()?;
172 let current_root = workspace(&directory, "current").or_fail()?;
173 let environment = TestEnvironment {
174 explicit_root: Some(directory.path().join("missing")),
175 manifest_dir: Some(manifest_root.join("crates/common")),
176 current_dir: current_root,
177 };
178
179 verify_that!(
180 workspace_root_with(&environment, "TEST_ROOT").as_deref(),
181 some(eq(&*manifest_root))
182 )
183 }
184
185 #[gtest]
186 fn current_directory_walks_to_workspace_ancestor() -> Result<()> {
187 let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
188 let root = workspace(&directory, "current").or_fail()?;
189 let nested = root.join("crates/common/src");
190
191 directory::ensure(&nested).or_fail()?;
192 let environment = TestEnvironment {
193 explicit_root: None,
194 manifest_dir: None,
195 current_dir: nested,
196 };
197
198 verify_that!(
199 workspace_root_with(&environment, "TEST_ROOT").as_deref(),
200 some(eq(&*root))
201 )
202 }
203
204 #[gtest]
205 fn current_directory_is_the_fallback_without_workspace_markers() -> Result<()> {
206 let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
207 let current_dir = directory.path().join("plain/directory");
208
209 directory::ensure(¤t_dir).or_fail()?;
210 let environment = TestEnvironment {
211 explicit_root: None,
212 manifest_dir: None,
213 current_dir: current_dir.clone(),
214 };
215
216 verify_that!(
217 workspace_root_with(&environment, "TEST_ROOT").as_deref(),
218 some(eq(&*current_dir))
219 )
220 }
221}