diff options
author | Daniel Schadt <kingdread@gmx.de> | 2021-11-09 18:38:52 +0100 |
---|---|---|
committer | Daniel Schadt <kingdread@gmx.de> | 2021-11-09 18:38:52 +0100 |
commit | 1db932c9f435fb54a3ca58333495d1e24ca7400f (patch) | |
tree | 875a532f80749e607837567b7b0dcb50fc22ca45 | |
parent | 7fbea3826e932cebd6c2f9883fe6ebdf8b08d6b2 (diff) | |
download | modderbaas-1db932c9f435fb54a3ca58333495d1e24ca7400f.tar.gz modderbaas-1db932c9f435fb54a3ca58333495d1e24ca7400f.tar.bz2 modderbaas-1db932c9f435fb54a3ca58333495d1e24ca7400f.zip |
split mod install logic
Previously, install_mod was a huge function that did a lot of things at
once. Not only did it do the actual mod copying, it also had the
dependency resolution, user interaction, ...
Now, we've split the code and made it more re-usable. The dependency
resolution is done in Baas::install, with special "hooks" being given in
baas::Installer that allow a consumer to customize the installation and
embed the user interaction there.
The code itself is pretty much the same, it is just split up now.
-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. |