aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modderbaas/src/baas.rs137
-rw-r--r--modderbaas/src/error.rs3
-rw-r--r--src/main.rs183
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.