use std::path::PathBuf;
use crate::catalog::Catalog;
use crate::env::Environment;
use crate::resolve;
const MAX_PLACES: usize = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Target {
pub path: PathBuf,
pub dirs: Vec<String>,
pub tools: Vec<String>,
pub create: bool,
pub git_root: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Unserved {
pub tool: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct Plan {
pub installed: Vec<String>,
pub targets: Vec<Target>,
pub duplicated: Vec<String>,
pub unserved: Vec<Unserved>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PlanError {
TooManyPlaces(usize),
}
impl std::fmt::Display for PlanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooManyPlaces(n) => write!(
f,
"{n} candidate places is more than the {MAX_PLACES} the planner compares"
),
}
}
}
impl std::error::Error for PlanError {}
struct Found {
place: Option<usize>,
refused: Option<String>,
}
struct Place {
path: PathBuf,
dirs: Vec<usize>,
preferred: bool,
exists: bool,
}
pub fn plan(catalog: &Catalog, env: &Environment) -> Result<Plan, PlanError> {
let installed: Vec<usize> = catalog
.tools
.iter()
.enumerate()
.filter(|(_, tool)| tool.home.resolve(env).is_dir())
.map(|(index, _)| index)
.collect();
let wanted = |dir: usize| {
let id = &catalog.dirs[dir].id;
installed.iter().any(|&tool| {
let tool = &catalog.tools[tool];
tool.reads.contains(id) || tool.may_read.contains(id)
})
};
let mut places: Vec<Place> = Vec::new();
let found: Vec<Found> = (0..catalog.dirs.len())
.map(|index| {
if !wanted(index) {
return Found {
place: None,
refused: Some("no installed tool reads it".to_string()),
};
}
locate(catalog, env, index, &mut places)
})
.collect();
if places.len() > MAX_PLACES {
return Err(PlanError::TooManyPlaces(places.len()));
}
let place_of = |id: &String| catalog.dir_index(id).and_then(|dir| found[dir].place);
let serving: Vec<(usize, Vec<usize>, Vec<usize>)> = installed
.iter()
.map(|&tool| {
let tool_entry = &catalog.tools[tool];
let reaches = tool_entry.reads.iter().filter_map(place_of).collect();
let sees = tool_entry
.reads
.iter()
.chain(&tool_entry.may_read)
.filter_map(place_of)
.collect();
(tool, reaches, sees)
})
.collect();
let unserved = unserved(catalog, &found, &serving);
let servable: Vec<&(usize, Vec<usize>, Vec<usize>)> = serving
.iter()
.filter(|(_, reaches, _)| !reaches.is_empty())
.collect();
let chosen = choose(&places, &servable);
let targets = chosen
.iter()
.map(|&place| {
let entry = &places[place];
Target {
path: entry.path.clone(),
dirs: entry
.dirs
.iter()
.map(|&dir| catalog.dirs[dir].id.clone())
.collect(),
tools: servable
.iter()
.filter(|(_, reaches, _)| reaches.contains(&place))
.map(|(tool, _, _)| catalog.tools[*tool].id.clone())
.collect(),
create: !entry.exists,
git_root: resolve::git_root(&entry.path),
}
})
.collect();
let duplicated = servable
.iter()
.filter(|(_, _, sees)| sees.iter().filter(|place| chosen.contains(place)).count() > 1)
.map(|(tool, _, _)| catalog.tools[*tool].id.clone())
.collect();
Ok(Plan {
installed: installed
.iter()
.map(|&tool| catalog.tools[tool].id.clone())
.collect(),
targets,
duplicated,
unserved,
})
}
fn unserved(
catalog: &Catalog,
found: &[Found],
serving: &[(usize, Vec<usize>, Vec<usize>)],
) -> Vec<Unserved> {
serving
.iter()
.filter(|(_, reaches, _)| reaches.is_empty())
.map(|(tool, _, _)| {
let tool = &catalog.tools[*tool];
let reasons: Vec<String> = tool
.reads
.iter()
.filter_map(|id| {
let dir = catalog.dir_index(id)?;
found[dir]
.refused
.as_ref()
.map(|why| format!("{id}: {why}"))
})
.collect();
Unserved {
tool: tool.id.clone(),
reason: reasons.join("; "),
}
})
.collect()
}
fn locate(catalog: &Catalog, env: &Environment, index: usize, places: &mut Vec<Place>) -> Found {
let dir = &catalog.dirs[index];
let path = dir.path.resolve(env);
let exists = path.is_dir();
if !exists
&& let Some(requires) = &dir.requires
&& !requires.resolve(env).is_dir()
{
return Found {
place: None,
refused: Some(format!(
"{} does not exist",
requires.resolve(env).display()
)),
};
}
match resolve::physical(&path) {
Ok(physical) => {
let place = places.iter().position(|place| place.path == physical);
let place = place.unwrap_or_else(|| {
places.push(Place {
path: physical,
dirs: Vec::new(),
preferred: false,
exists,
});
places.len() - 1
});
places[place].dirs.push(index);
places[place].preferred |= dir.preferred;
places[place].exists |= exists;
Found {
place: Some(place),
refused: None,
}
}
Err(error) => Found {
place: None,
refused: Some(format!("{} cannot be resolved: {error}", path.display())),
},
}
}
fn choose(places: &[Place], servable: &[&(usize, Vec<usize>, Vec<usize>)]) -> Vec<usize> {
if servable.is_empty() {
return Vec::new();
}
let mut best: Option<((usize, usize, usize), Vec<usize>)> = None;
for mask in 1u32..(1u32 << places.len()) {
let set: Vec<usize> = (0..places.len()).filter(|i| mask & (1 << i) != 0).collect();
if !servable
.iter()
.all(|(_, reaches, _)| reaches.iter().any(|place| set.contains(place)))
{
continue;
}
let duplicates = servable
.iter()
.filter(|(_, _, sees)| sees.iter().filter(|place| set.contains(place)).count() > 1)
.count();
let unpreferred = set
.iter()
.filter(|&&place| !places[place].preferred)
.count();
let score = (duplicates, set.len(), unpreferred);
if best.as_ref().is_none_or(|(known, _)| score < *known) {
best = Some((score, set));
}
}
best.map(|(_, set)| set).unwrap_or_default()
}
#[must_use]
pub fn existing_places(catalog: &Catalog, env: &Environment) -> Vec<PathBuf> {
let mut places: Vec<PathBuf> = Vec::new();
for dir in &catalog.dirs {
let path = dir.path.resolve(env);
if !path.is_dir() {
continue;
}
if let Ok(physical) = resolve::physical(&path)
&& !places.contains(&physical)
{
places.push(physical);
}
}
places
}