diff options
-rw-r--r-- | modderbaas/src/baas.rs | 137 | ||||
-rw-r--r-- | modderbaas/src/error.rs | 3 | ||||
-rw-r--r-- | src/main.rs | 183 |
3 files changed, 221 insertions, 102 deletions
diff --git a/modderbaas/src/baas.rs b/modderbaas/src/baas.rs index 938b4c4..c88e2b4 100644 --- a/modderbaas/src/baas.rs +++ b/modderbaas/src/baas.rs @@ -5,9 +5,10 @@ use dirs; use log::debug; use super::{ - error::Result, + download::{Downloader, Source}, + error::{Error, Result}, game::Game, - minemod::{self, MineMod}, + minemod::{self, MineMod, ModContainer}, scan, world::World, }; @@ -187,6 +188,86 @@ impl Baas { .collect::<Result<_>>()?, }) } + + /// Install the list of mods (given as [`Source`]s) into the given world using the given + /// [`Installer`]. + /// + /// The sources are downloaded using the given [`Downloader`]. + /// + /// This method exists to do most of the dependency resolution work, while still allowing + /// consumers to customize the installation process. + pub fn install<I: Installer, S: IntoIterator<Item = Source>>( + &self, + mut installer: I, + downloader: &Downloader, + world: &World, + sources: S, + ) -> Result<(), I::Error> { + let snapshot = self.snapshot()?; + let mut wanted = sources.into_iter().collect::<Vec<_>>(); + + let mut to_install = Vec::<Box<dyn ModContainer>>::new(); + let mut to_enable = Vec::<MineMod>::new(); + + let game_id = world.game_id()?; + let game = snapshot + .games() + .get(&game_id) + .ok_or(Error::GameNotFound(game_id))?; + let game_mods = game + .mods()? + .into_iter() + .map(|m| m.mod_id()) + .collect::<Result<Vec<_>, _>>()?; + + 'modloop: while !wanted.is_empty() { + let next_mod = wanted.remove(0); + + // Special handling for mods specified by their ID, as those could already exist. + if let Source::ModId(ref id) = next_mod { + if let Some(m) = snapshot.mods().get(id) { + // We have that one, just enable it and its dependencies! + to_enable.push(m.clone()); + wanted.extend(m.dependencies()?.into_iter().map(Source::ModId)); + continue; + } else if game_mods.contains(id) { + // This mod is already contained in the game, nothing for us to do + continue; + } + + // Is this a mod that is already queued for installation? + for mod_or_pack in &to_install { + for m in mod_or_pack.mods()? { + if &m.name()? == id { + continue 'modloop; + } + } + } + + // This mod is not available, so we let the installer resolve it + wanted.push(installer.resolve(id)?); + } else { + let downloaded = downloader.download(&next_mod)?; + for m in downloaded.mods()? { + wanted.extend(m.dependencies()?.into_iter().map(Source::ModId)); + } + to_install.push(downloaded); + } + } + + installer.display_changes(&to_install)?; + + for m in to_install { + let installed = installer.install_mod(&*m)?; + to_enable.extend(installed.mods()?); + } + + for m in to_enable { + installer.enable_mod(world, &m)?; + } + + Ok(()) + } } /// Snapshot of a [`Baas`] scan. @@ -222,3 +303,55 @@ impl Snapshot { &self.mods } } + +/// The [`Installer`] trait allows users of this library to customize the installation process. +/// +/// The hooks defined here are called by [`Baas::install`] to carry out the requested task. +#[allow(unused_variables)] +pub trait Installer { + /// The error that this installer may return. + /// + /// Any custom error type is acceptable, but we have to be able to return [`Error`]s from it. + type Error: From<Error>; + + /// Resolve the "short" mod ID to a proper mod. + /// + /// Since a [`Source::ModId`] can be ambiguous, the installer will call this function to + /// resolve those IDs. You can choose how to implement this, either by asking the user or by + /// using heuristics to select the right one. + /// + /// Note that returning another [`Source::ModId`] from this method is legal, but may result in + /// an endless loop. + fn resolve(&mut self, mod_id: &str) -> Result<Source, Self::Error>; + + /// Display the list of mods to be installed to the user. + /// + /// This is done before any changes are made to the world and can be used to get confirmation. + /// + /// If this function returns `Ok(())`, the process will continue. On any error reply, the + /// installation is cancelled. + /// + /// By default, this function simply returns `Ok(())`. + fn display_changes(&mut self, to_install: &[Box<dyn ModContainer>]) -> Result<(), Self::Error> { + Ok(()) + } + + /// Install the given mod or modpack, usually by copying the mod files to the appropriate + /// directory. + /// + /// This function is expected to return a mod object pointing to the copied mod. + /// + /// The purpose of this function is to customize the copying process, for example if + /// adjustments have to be made afterwards. + fn install_mod( + &mut self, + mod_or_pack: &dyn ModContainer, + ) -> Result<Box<dyn ModContainer>, Self::Error>; + + /// Enable the mod in the given world. + /// + /// This function per default calls [`World::enable_mod`]. + fn enable_mod(&mut self, world: &World, minemod: &MineMod) -> Result<(), Self::Error> { + Ok(world.enable_mod(&minemod.mod_id()?)?) + } +} diff --git a/modderbaas/src/error.rs b/modderbaas/src/error.rs index 5dbd6b6..670496c 100644 --- a/modderbaas/src/error.rs +++ b/modderbaas/src/error.rs @@ -38,6 +38,9 @@ pub enum Error { /// The given world does not have a game ID set. #[error("the world has no game ID set")] NoGameSet, + /// The game with the given ID could not be found. + #[error("the game with ID '{0}' was not found")] + GameNotFound(String), /// ContentDB returned more than one fitting mod for the query. #[error("the mod ID '{0}' does not point to a single mod")] AmbiguousModId(String), diff --git a/src/main.rs b/src/main.rs index 87f8847..5b8c19c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,9 @@ use log::debug; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; use modderbaas::{ - minemod::ModContainer, util, Baas, ContentDb, Downloader, MineMod, Snapshot, Source, World, + baas::{Baas, Installer}, + minemod::{self, ModContainer}, + util, ContentDb, Downloader, MineMod, Snapshot, Source, World, }; fn main() -> Result<()> { @@ -96,7 +98,7 @@ fn main() -> Result<()> { if let Some(install) = matches.subcommand_matches("install") { let mods = install.values_of("mod").unwrap().collect::<Vec<_>>(); - install_mods(&mut stdout, &snapshot, &world, &mods, install)?; + install_mods(&mut stdout, &baas, &world, &mods, install)?; } Ok(()) @@ -207,121 +209,88 @@ fn enable_mods( /// Install the given mods, installing dependencies if needed. fn install_mods( output: &mut StandardStream, - snapshot: &Snapshot, + baas: &Baas, world: &World, mods: &[&str], matches: &ArgMatches, ) -> Result<()> { let target_dir = Path::new(matches.value_of("target").unwrap()); let dry_run = matches.is_present("dry-run"); - let chown_after = matches.is_present("chown"); + let chown = matches.is_present("chown"); let content_db = ContentDb::new(); let downloader = Downloader::new()?; - let mut wanted = mods + let sources = mods .iter() .map(|&s| Source::from_str(s)) .collect::<Result<Vec<_>, _>>()?; - let mut to_install = Vec::<Box<dyn ModContainer>>::new(); - let mut to_enable = Vec::<MineMod>::new(); + let installer = InteractiveInstaller { + output, + content_db, + target_dir, + dry_run, + chown, + }; + baas.install(installer, &downloader, world, sources)?; - let game = snapshot - .games() - .get(&world.game_id()?) - .ok_or_else(|| anyhow!("The game definition was not found"))?; - let game_mods = game - .mods()? - .into_iter() - .map(|m| m.mod_id()) - .collect::<Result<Vec<_>, _>>()?; + writeln!(output, "Done!")?; - 'modloop: while !wanted.is_empty() { - let next_mod = wanted.remove(0); + Ok(()) +} - // Special handling for mods specified by their ID, as those could already exist. - if let Source::ModId(ref id) = next_mod { - if let Some(m) = snapshot.mods().get(id) { - // We have that one, just enable it and its dependencies! - to_enable.push(m.clone()); - wanted.extend(m.dependencies()?.into_iter().map(Source::ModId)); - continue; - } else if game_mods.contains(id) { - // This mod is already contained in the game, nothing for us to do - continue; - } +/// The installer that interactively asks the user about choices. +struct InteractiveInstaller<'o, 'p> { + output: &'o mut StandardStream, + content_db: ContentDb, + target_dir: &'p Path, + dry_run: bool, + chown: bool, +} - // Is this a mod that is already queued for installation? - for mod_or_pack in &to_install { - for m in mod_or_pack.mods()? { - if &m.name()? == id { - continue 'modloop; - } - } - } +impl<'o, 'p> Installer for InteractiveInstaller<'o, 'p> { + type Error = anyhow::Error; - // This mod is not available, so we search the content DB - writeln!(output, "Searching for candidates: {}", id)?; - let candidates = content_db.resolve(id)?; - if candidates.is_empty() { - bail!("Could not find a suitable mod for '{}'", id); - } else if candidates.len() == 1 { - wanted.push(Source::Http(candidates.into_iter().next().unwrap().url)); - } else { - let items = candidates - .into_iter() - .map(|c| { - ( - format!("{} by {} - {}", c.title, c.author, c.short_description), - c, - ) - }) - .collect::<Vec<_>>(); - writeln!( - output, - "{} candidates found, please select one:", - items.len() - )?; - let candidate = user_choice(&items, output)?; - wanted.push(Source::Http(candidate.url.clone())); - } + fn resolve(&mut self, mod_id: &str) -> Result<Source> { + writeln!(&mut self.output, "Searching for candidates: {}", mod_id)?; + + let candidates = self.content_db.resolve(mod_id)?; + if candidates.is_empty() { + bail!("Could not find a suitable mod for '{}'", mod_id); + } else if candidates.len() == 1 { + Ok(Source::Http(candidates.into_iter().next().unwrap().url)) } else { - let downloaded = downloader - .download(&next_mod) - .context("Failed to download mod")?; - for m in downloaded.mods()? { - wanted.extend(m.dependencies()?.into_iter().map(Source::ModId)); - } - to_install.push(downloaded); + let items = candidates + .into_iter() + .map(|c| { + ( + format!("{} by {} - {}", c.title, c.author, c.short_description), + c, + ) + }) + .collect::<Vec<_>>(); + writeln!( + &mut self.output, + "{} candidates found, please select one:", + items.len() + )?; + let candidate = user_choice(&items, self.output)?; + Ok(Source::Http(candidate.url.clone())) } } - writeln!(output, "Installing {} new mods:", to_install.len())?; - writeln!(output, "{}", to_install.iter().join(", "))?; - - ask_continue(output)?; + fn install_mod(&mut self, mod_or_pack: &dyn ModContainer) -> Result<Box<dyn ModContainer>> { + let mod_id = mod_or_pack.name()?; + writeln!(&mut self.output, "Installing {}", mod_id)?; - if dry_run { - for m in to_install { - let mod_id = m.name()?; - let mod_dir = target_dir.join(&mod_id); - writeln!(output, "Installing {} to {:?}", m, mod_dir)?; - to_enable.extend(m.mods()?); + if self.dry_run { + // Re-open so we get a fresh Box<> + return Ok(minemod::open_mod_or_pack(mod_or_pack.path())?); } - for m in to_enable { - let mod_id = m.mod_id()?; - writeln!(output, "Enabling {}", mod_id)?; - } - return Ok(()); - } - for m in to_install { - let mod_id = m.name()?; - writeln!(output, "Installing {}", mod_id)?; - let installed = m - .install_to(target_dir) + let installed = mod_or_pack + .install_to(self.target_dir) .context(format!("Error installing '{}'", mod_id))?; - to_enable.extend(installed.mods()?); #[cfg(unix)] { @@ -329,24 +298,38 @@ fn install_mods( sys::stat, unistd::{Gid, Uid}, }; - if chown_after { - let perms = stat::stat(target_dir)?; + if self.chown { + let perms = stat::stat(self.target_dir)?; let (uid, gid) = (Uid::from_raw(perms.st_uid), Gid::from_raw(perms.st_gid)); util::chown_recursive(installed.path(), Some(uid), Some(gid))?; } } + + Ok(installed) } - for m in to_enable { - let mod_id = m.mod_id()?; - world - .enable_mod(&mod_id) - .context(format!("Error enabling '{}'", mod_id))?; + fn display_changes(&mut self, to_install: &[Box<dyn ModContainer>]) -> Result<()> { + writeln!( + &mut self.output, + "Installing {} new mods:", + to_install.len() + )?; + writeln!(&mut self.output, "{}", to_install.iter().join(", "))?; + + ask_continue(self.output) } - writeln!(output, "Done!")?; + fn enable_mod(&mut self, world: &World, minemod: &MineMod) -> Result<()> { + let mod_id = minemod.mod_id()?; + writeln!(&mut self.output, "Enabling {}", mod_id)?; - Ok(()) + if !self.dry_run { + world + .enable_mod(&mod_id) + .context(format!("Error enabling '{}'", mod_id))?; + } + Ok(()) + } } /// Presents the user with a choice of items and awaits a selection. |