use std::collections::{HashMap, HashSet, VecDeque};
use std::path::{Path, PathBuf};
use crate::{DEFAULT_REGISTRY, Dependency, Registry, content_hash};
use super::Confirm;
use super::module;
use super::package::Package;
use super::state::{InstallState, InstalledComponent};
use super::workspace::Workspace;
pub struct AddOptions {
pub components: Vec<String>,
pub registry: Option<String>,
pub overwrite: bool,
}
pub struct AddedComponent {
pub name: String,
pub file: PathBuf,
pub registry: String,
}
pub enum AddOutcome {
UpToDate,
Added(Vec<AddedComponent>),
}
struct Pending {
registry: String,
component: String,
root: bool,
}
struct PlannedWrite {
name: String,
dir: PathBuf,
file: PathBuf,
relative_file: PathBuf,
file_name: String,
contents: String,
registry: String,
}
struct PlannedRemoval {
file: PathBuf,
dir: PathBuf,
file_name: String,
}
pub fn add(
package: &Package,
options: &AddOptions,
confirm: &mut Confirm<'_>,
) -> Result<AddOutcome, String> {
let mut state = InstallState::load(package)?;
let workspace = Workspace::load(package)?;
let mut registries: HashMap<String, Registry> = HashMap::new();
let mut visited: HashSet<(String, String)> = HashSet::new();
let mut queue: VecDeque<Pending> = VecDeque::new();
for component in &options.components {
let root_registry = resolve_root_registry(
component,
options.registry.as_deref(),
&workspace,
&mut registries,
confirm,
)?;
queue.push_back(Pending {
registry: root_registry,
component: component.clone(),
root: true,
});
}
let mut writes: Vec<PlannedWrite> = Vec::new();
let mut removals: Vec<PlannedRemoval> = Vec::new();
while let Some(pending) = queue.pop_front() {
if !visited.insert((pending.registry.clone(), pending.component.clone())) {
continue;
}
let components_dir = state.components_dir.clone();
let registry = load_registry(&mut registries, &workspace, &pending.registry)?;
let component = registry.get(&pending.component).ok_or_else(|| {
let available: Vec<&str> = registry.names().collect();
format!(
"unknown component `{}` in registry `{}`; available: {}",
pending.component,
pending.registry,
available.join(", ")
)
})?;
let relative_file = components_dir.join(component.file_name());
let dir = package.resolve(&components_dir);
let file = dir.join(component.file_name());
let mut replacing = false;
if let Some((other_registry, other_component)) =
find_file_conflict(&state, &pending.registry, component.name(), &relative_file)
{
let prompt = format!(
"{} is already provided by `{other_component}` from `{other_registry}`. Replace it with `{}` from `{}`?",
relative_file.display(),
component.name(),
pending.registry
);
if !confirm(&prompt)? {
return Err(format!(
"aborted; {} is already provided by `{other_component}` from `{other_registry}`",
relative_file.display()
));
}
let registry = state
.registries
.get_mut(&other_registry)
.expect("conflicting registry exists");
if let Some(removed) = registry.components.remove(&other_component)
&& let Some(file_name) = removed.file.file_name().and_then(|name| name.to_str())
{
removals.push(PlannedRemoval {
file: package.resolve(&removed.file),
dir: package.resolve(&components_dir),
file_name: file_name.to_string(),
});
}
replacing = true;
}
let exists = file.exists();
if exists && pending.root && !options.overwrite && !replacing {
return Err(format!(
"{} already exists; pass --overwrite to replace it",
relative_file.display()
));
}
let contents = component
.read_source()
.map_err(|error| format!("failed to read component `{}`: {error}", component.name()))?;
let hash = content_hash(&contents);
if !exists || (pending.root && options.overwrite) || replacing {
writes.push(PlannedWrite {
name: component.name().to_string(),
dir: dir.clone(),
file: file.clone(),
relative_file: relative_file.clone(),
file_name: component.file_name().to_string(),
contents,
registry: pending.registry.clone(),
});
}
let dependencies = component.dependencies().to_vec();
state.registry_mut(&pending.registry).components.insert(
component.name().to_string(),
InstalledComponent {
hash,
file: relative_file,
},
);
for dependency in dependencies {
let (registry, component) = match dependency {
Dependency::Same(name) => (pending.registry.clone(), name),
Dependency::Other { registry, name } => (registry, name),
};
queue.push_back(Pending {
registry,
component,
root: false,
});
}
}
module::check(&package.resolve(&state.components_dir))?;
for removal in &removals {
match std::fs::remove_file(&removal.file) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(format!(
"failed to remove {}: {error}",
removal.file.display()
));
}
}
module::undeclare(&removal.dir, &removal.file_name)?;
}
for write in &writes {
std::fs::create_dir_all(&write.dir)
.map_err(|error| format!("failed to create {}: {error}", write.dir.display()))?;
std::fs::write(&write.file, &write.contents)
.map_err(|error| format!("failed to write {}: {error}", write.file.display()))?;
module::declare(&write.dir, &write.file_name)?;
}
state.save(package)?;
if writes.is_empty() {
Ok(AddOutcome::UpToDate)
} else {
Ok(AddOutcome::Added(
writes
.into_iter()
.map(|write| AddedComponent {
name: write.name,
file: write.relative_file,
registry: write.registry,
})
.collect(),
))
}
}
fn resolve_root_registry(
component: &str,
registry: Option<&str>,
workspace: &Workspace,
registries: &mut HashMap<String, Registry>,
confirm: &mut Confirm<'_>,
) -> Result<String, String> {
if let Some(name) = registry {
let loaded = load_registry(registries, workspace, name)?;
if loaded.get(component).is_none() {
let available: Vec<&str> = loaded.names().collect();
return Err(format!(
"unknown component `{component}` in registry `{name}`; available: {}",
available.join(", ")
));
}
return Ok(name.to_string());
}
let default = DEFAULT_REGISTRY;
let offers_default = match load_registry(registries, workspace, default) {
Ok(registry) => registry.get(component).is_some(),
Err(_) => false,
};
if offers_default {
return Ok(default.to_string());
}
let others: Vec<String> = workspace
.available_registries()
.into_iter()
.filter(|name| name != default)
.collect();
let mut offering: Vec<String> = Vec::new();
for name in &others {
let offers = match load_registry(registries, workspace, name) {
Ok(registry) => registry.get(component).is_some(),
Err(_) => false,
};
if offers {
offering.push(name.clone());
}
}
match offering.as_slice() {
[] => Err(format!(
"unknown component `{component}`: not in the default registry `{default}` or any dependency registry"
)),
[name] => {
let prompt = format!(
"`{component}` is not in the default registry `{default}`. Add it from `{name}` instead?"
);
if confirm(&prompt)? {
Ok(name.clone())
} else {
Err(format!(
"aborted; pass `--registry {name}` to add `{component}` from it"
))
}
}
many => Err(format!(
"`{component}` is not in the default registry `{default}` but is available in {}; pass --registry to choose",
many.join(", ")
)),
}
}
fn find_file_conflict(
state: &InstallState,
registry: &str,
component: &str,
file: &Path,
) -> Option<(String, String)> {
for (registry_name, registry_state) in &state.registries {
for (component_name, installed) in ®istry_state.components {
if (registry_name.as_str(), component_name.as_str()) != (registry, component)
&& installed.file == file
{
return Some((registry_name.clone(), component_name.clone()));
}
}
}
None
}
fn load_registry<'a>(
cache: &'a mut HashMap<String, Registry>,
workspace: &Workspace,
name: &str,
) -> Result<&'a Registry, String> {
if !cache.contains_key(name) {
let dir = workspace.registry_dir(name)?;
let loaded = Registry::load(dir)
.map_err(|error| format!("failed to load registry `{name}`: {error}"))?;
cache.insert(name.to_string(), loaded);
}
Ok(&cache[name])
}