Improved mod resolution

This commit is contained in:
Warren Hood 2024-08-17 22:06:31 +02:00
parent 09e3c066b6
commit ec50a380b8
6 changed files with 103 additions and 68 deletions

View file

@ -98,6 +98,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
mc_modpack_meta = mc_modpack_meta.provider(provider); mc_modpack_meta = mc_modpack_meta.provider(provider);
} }
mc_modpack_meta.init_project(&dir)?; mc_modpack_meta.init_project(&dir)?;
let modpack_lock = resolver::PinnedPackMeta::load_from_directory(&dir).await?;
modpack_lock.save_to_dir(&dir)?;
} }
Commands::New { Commands::New {
name, name,
@ -118,6 +120,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
mc_modpack_meta = mc_modpack_meta.provider(provider); mc_modpack_meta = mc_modpack_meta.provider(provider);
} }
mc_modpack_meta.init_project(&dir)?; mc_modpack_meta.init_project(&dir)?;
let modpack_lock = resolver::PinnedPackMeta::load_from_directory(&dir).await?;
modpack_lock.save_to_dir(&dir)?;
} }
Commands::Add { Commands::Add {
name, name,
@ -147,7 +152,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
match resolver::PinnedPackMeta::load_from_current_directory().await { match resolver::PinnedPackMeta::load_from_current_directory().await {
Ok(mut modpack_lock) => { Ok(mut modpack_lock) => {
let pin_result = modpack_lock.pin_mod(&mod_meta, &modpack_meta).await; let pin_result = modpack_lock.pin_mod_and_deps(&mod_meta, &modpack_meta).await;
if let Err(e) = pin_result { if let Err(e) = pin_result {
revert_modpack_meta(e); revert_modpack_meta(e);
} }

View file

@ -25,7 +25,7 @@ impl std::str::FromStr for ModProvider {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ModMeta { pub struct ModMeta {
pub name: String, pub name: String,
pub version: String, pub version: String,

View file

@ -81,7 +81,7 @@ impl ModpackMeta {
pub fn add_mod(mut self, mod_meta: &ModMeta) -> Self { pub fn add_mod(mut self, mod_meta: &ModMeta) -> Self {
if let Some(old_mod_meta) = self.mods.get(&mod_meta.name) { if let Some(old_mod_meta) = self.mods.get(&mod_meta.name) {
println!("Updating {} version {}->{}", mod_meta.name, old_mod_meta.version, mod_meta.version); println!("Updating {} version constraints: {} -> {}", mod_meta.name, old_mod_meta.version, mod_meta.version);
} }
else { else {
println!( println!(

View file

@ -5,18 +5,18 @@ use crate::mod_meta::ModMeta;
pub mod modrinth; pub mod modrinth;
pub mod raw; pub mod raw;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Clone)]
enum FileSource { enum FileSource {
Download { url: String, sha1: String, sha512: String}, Download { url: String, sha1: String, sha512: String},
Local { path: PathBuf, sha1: String, sha512: String }, Local { path: PathBuf, sha1: String, sha512: String },
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Clone)]
pub struct PinnedMod { pub struct PinnedMod {
/// Source of the files for the mod /// Source of the files for the mod
source: Vec<FileSource>, source: Vec<FileSource>,
/// Version of mod /// Version of mod
version: semver::Version, pub version: semver::Version,
/// Pinned dependencies of a pinned mod /// Pinned dependencies of a pinned mod
deps: Option<Vec<PinnedMod>> pub deps: Option<Vec<ModMeta>>
} }

View file

@ -58,55 +58,40 @@ impl Modrinth {
&self, &self,
mod_meta: &ModMeta, mod_meta: &ModMeta,
pack_meta: &ModpackMeta, pack_meta: &ModpackMeta,
) -> Result<Vec<PinnedMod>, Box<dyn Error>> { ) -> Result<PinnedMod, Box<dyn Error>> {
let mut versions = self.get_project_versions(&mod_meta.name, pack_meta).await?; let mut versions = self.get_project_versions(&mod_meta.name, pack_meta).await?;
versions.sort_by_key(|v| v.version_number.clone()); versions.sort_by_key(|v| v.version_number.clone());
versions.reverse(); versions.reverse();
if mod_meta.version == "*" { let package = if mod_meta.version == "*" {
return Ok(versions versions
.into_iter() .last()
.map(|v| PinnedMod { .ok_or(format!("Cannot find package {}", mod_meta.name))?
source: v } else {
.files let expected_version = semver::Version::parse(&mod_meta.version)?;
.into_iter() versions
.map(|f| FileSource::Download { .iter()
url: f.url, .filter(|v| v.version_number == expected_version)
sha1: f.hashes.sha1, .nth(0)
sha512: f.hashes.sha512, .ok_or(format!(
}) "Cannot find package {}@{}",
.collect(), mod_meta.name, mod_meta.version
version: v.version_number, ))?
deps: None, // TODO: Implement automagic transitive dep installation and pinning };
Ok(PinnedMod {
source: package
.files
.iter()
.map(|f| FileSource::Download {
url: f.url.clone(),
sha1: f.hashes.sha1.clone(),
sha512: f.hashes.sha512.clone(),
}) })
.collect()); .collect(),
} version: package.version_number.clone(),
deps: None, // TODO: Get deps
// TODO: Implement more general version constraints })
let expected_version = semver::Version::parse(&mod_meta.version)?;
for v in versions.into_iter() {
if v.version_number == expected_version {
return Ok(vec![PinnedMod {
source: v
.files
.into_iter()
.map(|f| FileSource::Download {
url: f.url,
sha1: f.hashes.sha1,
sha512: f.hashes.sha512,
})
.collect(),
version: v.version_number,
deps: None,
}]);
}
}
Err(format!(
"Couldn't find a version for mod '{}' that satisfies the version constraint `{}`",
mod_meta.name, mod_meta.version
)
.into())
} }
async fn get_project_versions( async fn get_project_versions(

View file

@ -1,6 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
borrow::Borrow,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
error::Error, error::Error,
path::PathBuf, path::PathBuf,
@ -29,11 +28,46 @@ impl PinnedPackMeta {
} }
} }
pub async fn pin_mod( pub async fn pin_mod_and_deps(
&mut self, &mut self,
mod_metadata: &ModMeta, mod_metadata: &ModMeta,
pack_metadata: &ModpackMeta, pack_metadata: &ModpackMeta,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
if let Some(mod_meta) = self.mods.get(&mod_metadata.name) {
if mod_metadata.version != "*"
&& semver::Version::parse(&mod_metadata.version)? == mod_meta.version
{
// Skip already pinned mods
// TODO: Replace * with the current mod version in the modpack meta so this doesn't get called twice for the first mod created
return Ok(());
}
}
let mut deps =
HashSet::from_iter(self.pin_mod(mod_metadata, pack_metadata).await?.into_iter());
while !deps.is_empty() {
let mut next_deps = HashSet::new();
for dep in deps.iter() {
println!(
"Adding mod {}@{} (dependency of {}@{})",
dep.name, dep.version, mod_metadata.name, mod_metadata.version
);
next_deps.extend(self.pin_mod(dep, &pack_metadata).await?);
}
deps = next_deps;
}
Ok(())
}
/// Pin a mod version
///
/// A list of dependencies to pin is included
pub async fn pin_mod(
&mut self,
mod_metadata: &ModMeta,
pack_metadata: &ModpackMeta,
) -> Result<Vec<ModMeta>, Box<dyn Error>> {
let mod_providers = if let Some(mod_providers) = &mod_metadata.providers { let mod_providers = if let Some(mod_providers) = &mod_metadata.providers {
mod_providers mod_providers
} else { } else {
@ -52,28 +86,35 @@ impl PinnedPackMeta {
match mod_provider { match mod_provider {
crate::mod_meta::ModProvider::CurseForge => unimplemented!(), crate::mod_meta::ModProvider::CurseForge => unimplemented!(),
crate::mod_meta::ModProvider::Modrinth => { crate::mod_meta::ModProvider::Modrinth => {
let pinned_mods = self.modrinth.resolve(&mod_metadata, pack_metadata).await; let pinned_mod = self.modrinth.resolve(&mod_metadata, pack_metadata).await;
if let Ok(pinned_mods) = pinned_mods { if let Ok(pinned_mod) = pinned_mod {
pinned_mods.into_iter().for_each(|m| { self.mods
self.mods.insert(mod_metadata.name.clone(), m); .insert(mod_metadata.name.clone(), pinned_mod.clone());
}); println!("Pinned {}@{}", mod_metadata.name, pinned_mod.version);
} else if let Err(e) = pinned_mods { if let Some(deps) = &pinned_mod.deps {
return Err(format!( return Ok(deps
"Failed to resolve mod '{}' (provider={:#?}) with constraint {}: {}", .iter()
mod_metadata.name, mod_provider, mod_metadata.version, e .filter(|d| !self.mods.contains_key(&d.name))
) .cloned()
.into()); .collect());
}
return Ok(vec![]);
} }
} }
crate::mod_meta::ModProvider::Raw => unimplemented!(), crate::mod_meta::ModProvider::Raw => unimplemented!(),
}; };
} }
Ok(())
Err(format!(
"Failed to pin mod '{}' (providers={:#?}) with constraint {} and all its deps",
mod_metadata.name, mod_metadata.providers, mod_metadata.version
)
.into())
} }
pub async fn init(&mut self, modpack_meta: &ModpackMeta) -> Result<(), Box<dyn Error>> { pub async fn init(&mut self, modpack_meta: &ModpackMeta) -> Result<(), Box<dyn Error>> {
for mod_meta in modpack_meta.iter_mods() { for mod_meta in modpack_meta.iter_mods() {
self.pin_mod(mod_meta, modpack_meta).await?; self.pin_mod_and_deps(mod_meta, modpack_meta).await?;
} }
Ok(()) Ok(())
} }
@ -88,8 +129,12 @@ impl PinnedPackMeta {
} }
pub fn save_current_dir_lock(&self) -> Result<(), Box<dyn Error>> { pub fn save_current_dir_lock(&self) -> Result<(), Box<dyn Error>> {
let modpack_lock_file_path = self.save_to_dir(&std::env::current_dir()?)?;
std::env::current_dir()?.join(PathBuf::from(MODPACK_LOCK_FILENAME)); Ok(())
}
pub fn save_to_dir(&self, dir: &PathBuf) -> Result<(), Box<dyn Error>> {
let modpack_lock_file_path = dir.join(PathBuf::from(MODPACK_LOCK_FILENAME));
self.save_to_file(&modpack_lock_file_path)?; self.save_to_file(&modpack_lock_file_path)?;
Ok(()) Ok(())
} }