1use std::path::PathBuf;
12
13use crate::catalog::Catalog;
14use crate::env::Environment;
15use crate::resolve;
16
17const MAX_PLACES: usize = 16;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24#[non_exhaustive]
25pub struct Target {
26 pub path: PathBuf,
29 pub dirs: Vec<String>,
32 pub tools: Vec<String>,
34 pub create: bool,
36 pub git_root: Option<PathBuf>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42#[non_exhaustive]
43pub struct Unserved {
44 pub tool: String,
46 pub reason: String,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Default)]
52#[non_exhaustive]
53pub struct Plan {
54 pub installed: Vec<String>,
56 pub targets: Vec<Target>,
58 pub duplicated: Vec<String>,
61 pub unserved: Vec<Unserved>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67#[non_exhaustive]
68pub enum PlanError {
69 TooManyPlaces(usize),
71}
72
73impl std::fmt::Display for PlanError {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 Self::TooManyPlaces(n) => write!(
77 f,
78 "{n} candidate places is more than the {MAX_PLACES} the planner compares"
79 ),
80 }
81 }
82}
83
84impl std::error::Error for PlanError {}
85
86struct Found {
88 place: Option<usize>,
90 refused: Option<String>,
92}
93
94struct Place {
96 path: PathBuf,
97 dirs: Vec<usize>,
98 preferred: bool,
99 exists: bool,
100}
101
102pub fn plan(catalog: &Catalog, env: &Environment) -> Result<Plan, PlanError> {
107 let installed: Vec<usize> = catalog
108 .tools
109 .iter()
110 .enumerate()
111 .filter(|(_, tool)| tool.home.resolve(env).is_dir())
112 .map(|(index, _)| index)
113 .collect();
114
115 let wanted = |dir: usize| {
118 let id = &catalog.dirs[dir].id;
119 installed.iter().any(|&tool| {
120 let tool = &catalog.tools[tool];
121 tool.reads.contains(id) || tool.may_read.contains(id)
122 })
123 };
124
125 let mut places: Vec<Place> = Vec::new();
126 let found: Vec<Found> = (0..catalog.dirs.len())
127 .map(|index| {
128 if !wanted(index) {
129 return Found {
130 place: None,
131 refused: Some("no installed tool reads it".to_string()),
132 };
133 }
134 locate(catalog, env, index, &mut places)
135 })
136 .collect();
137
138 if places.len() > MAX_PLACES {
139 return Err(PlanError::TooManyPlaces(places.len()));
140 }
141
142 let place_of = |id: &String| catalog.dir_index(id).and_then(|dir| found[dir].place);
147 let serving: Vec<(usize, Vec<usize>, Vec<usize>)> = installed
148 .iter()
149 .map(|&tool| {
150 let tool_entry = &catalog.tools[tool];
151 let reaches = tool_entry.reads.iter().filter_map(place_of).collect();
152 let sees = tool_entry
153 .reads
154 .iter()
155 .chain(&tool_entry.may_read)
156 .filter_map(place_of)
157 .collect();
158 (tool, reaches, sees)
159 })
160 .collect();
161
162 let unserved = unserved(catalog, &found, &serving);
163
164 let servable: Vec<&(usize, Vec<usize>, Vec<usize>)> = serving
165 .iter()
166 .filter(|(_, reaches, _)| !reaches.is_empty())
167 .collect();
168 let chosen = choose(&places, &servable);
169
170 let targets = chosen
171 .iter()
172 .map(|&place| {
173 let entry = &places[place];
174 Target {
175 path: entry.path.clone(),
176 dirs: entry
177 .dirs
178 .iter()
179 .map(|&dir| catalog.dirs[dir].id.clone())
180 .collect(),
181 tools: servable
182 .iter()
183 .filter(|(_, reaches, _)| reaches.contains(&place))
184 .map(|(tool, _, _)| catalog.tools[*tool].id.clone())
185 .collect(),
186 create: !entry.exists,
187 git_root: resolve::git_root(&entry.path),
188 }
189 })
190 .collect();
191
192 let duplicated = servable
193 .iter()
194 .filter(|(_, _, sees)| sees.iter().filter(|place| chosen.contains(place)).count() > 1)
195 .map(|(tool, _, _)| catalog.tools[*tool].id.clone())
196 .collect();
197
198 Ok(Plan {
199 installed: installed
200 .iter()
201 .map(|&tool| catalog.tools[tool].id.clone())
202 .collect(),
203 targets,
204 duplicated,
205 unserved,
206 })
207}
208
209fn unserved(
211 catalog: &Catalog,
212 found: &[Found],
213 serving: &[(usize, Vec<usize>, Vec<usize>)],
214) -> Vec<Unserved> {
215 serving
216 .iter()
217 .filter(|(_, reaches, _)| reaches.is_empty())
218 .map(|(tool, _, _)| {
219 let tool = &catalog.tools[*tool];
220 let reasons: Vec<String> = tool
221 .reads
222 .iter()
223 .filter_map(|id| {
224 let dir = catalog.dir_index(id)?;
225 found[dir]
226 .refused
227 .as_ref()
228 .map(|why| format!("{id}: {why}"))
229 })
230 .collect();
231 Unserved {
232 tool: tool.id.clone(),
233 reason: reasons.join("; "),
234 }
235 })
236 .collect()
237}
238
239fn locate(catalog: &Catalog, env: &Environment, index: usize, places: &mut Vec<Place>) -> Found {
241 let dir = &catalog.dirs[index];
242 let path = dir.path.resolve(env);
243 let exists = path.is_dir();
244 if !exists
245 && let Some(requires) = &dir.requires
246 && !requires.resolve(env).is_dir()
247 {
248 return Found {
251 place: None,
252 refused: Some(format!(
253 "{} does not exist",
254 requires.resolve(env).display()
255 )),
256 };
257 }
258 match resolve::physical(&path) {
259 Ok(physical) => {
260 let place = places.iter().position(|place| place.path == physical);
261 let place = place.unwrap_or_else(|| {
262 places.push(Place {
263 path: physical,
264 dirs: Vec::new(),
265 preferred: false,
266 exists,
267 });
268 places.len() - 1
269 });
270 places[place].dirs.push(index);
271 places[place].preferred |= dir.preferred;
272 places[place].exists |= exists;
273 Found {
274 place: Some(place),
275 refused: None,
276 }
277 }
278 Err(error) => Found {
279 place: None,
280 refused: Some(format!("{} cannot be resolved: {error}", path.display())),
281 },
282 }
283}
284
285fn choose(places: &[Place], servable: &[&(usize, Vec<usize>, Vec<usize>)]) -> Vec<usize> {
292 if servable.is_empty() {
293 return Vec::new();
294 }
295 let mut best: Option<((usize, usize, usize), Vec<usize>)> = None;
296 for mask in 1u32..(1u32 << places.len()) {
297 let set: Vec<usize> = (0..places.len()).filter(|i| mask & (1 << i) != 0).collect();
298 if !servable
299 .iter()
300 .all(|(_, reaches, _)| reaches.iter().any(|place| set.contains(place)))
301 {
302 continue;
303 }
304 let duplicates = servable
305 .iter()
306 .filter(|(_, _, sees)| sees.iter().filter(|place| set.contains(place)).count() > 1)
307 .count();
308 let unpreferred = set
309 .iter()
310 .filter(|&&place| !places[place].preferred)
311 .count();
312 let score = (duplicates, set.len(), unpreferred);
313 if best.as_ref().is_none_or(|(known, _)| score < *known) {
317 best = Some((score, set));
318 }
319 }
320 best.map(|(_, set)| set).unwrap_or_default()
321}
322
323#[must_use]
330pub fn existing_places(catalog: &Catalog, env: &Environment) -> Vec<PathBuf> {
331 let mut places: Vec<PathBuf> = Vec::new();
332 for dir in &catalog.dirs {
333 let path = dir.path.resolve(env);
334 if !path.is_dir() {
335 continue;
336 }
337 if let Ok(physical) = resolve::physical(&path)
338 && !places.contains(&physical)
339 {
340 places.push(physical);
341 }
342 }
343 places
344}