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.init_project(&dir)?;
let modpack_lock = resolver::PinnedPackMeta::load_from_directory(&dir).await?;
modpack_lock.save_to_dir(&dir)?;
}
Commands::New {
name,
@ -118,6 +120,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
mc_modpack_meta = mc_modpack_meta.provider(provider);
}
mc_modpack_meta.init_project(&dir)?;
let modpack_lock = resolver::PinnedPackMeta::load_from_directory(&dir).await?;
modpack_lock.save_to_dir(&dir)?;
}
Commands::Add {
name,
@ -147,7 +152,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
match resolver::PinnedPackMeta::load_from_current_directory().await {
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 {
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 name: String,
pub version: String,

View file

@ -81,7 +81,7 @@ impl ModpackMeta {
pub fn add_mod(mut self, mod_meta: &ModMeta) -> Self {
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 {
println!(

View file

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

View file

@ -58,55 +58,40 @@ impl Modrinth {
&self,
mod_meta: &ModMeta,
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?;
versions.sort_by_key(|v| v.version_number.clone());
versions.reverse();
if mod_meta.version == "*" {
return Ok(versions
.into_iter()
.map(|v| 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, // TODO: Implement automagic transitive dep installation and pinning
})
.collect());
}
// TODO: Implement more general version constraints
let package = if mod_meta.version == "*" {
versions
.last()
.ok_or(format!("Cannot find package {}", mod_meta.name))?
} else {
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
versions
.iter()
.filter(|v| v.version_number == expected_version)
.nth(0)
.ok_or(format!(
"Cannot find package {}@{}",
mod_meta.name, mod_meta.version
))?
};
Ok(PinnedMod {
source: package
.files
.into_iter()
.iter()
.map(|f| FileSource::Download {
url: f.url,
sha1: f.hashes.sha1,
sha512: f.hashes.sha512,
url: f.url.clone(),
sha1: f.hashes.sha1.clone(),
sha512: f.hashes.sha512.clone(),
})
.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())
version: package.version_number.clone(),
deps: None, // TODO: Get deps
})
}
async fn get_project_versions(

View file

@ -1,6 +1,5 @@
use serde::{Deserialize, Serialize};
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
error::Error,
path::PathBuf,
@ -29,11 +28,46 @@ impl PinnedPackMeta {
}
}
pub async fn pin_mod(
pub async fn pin_mod_and_deps(
&mut self,
mod_metadata: &ModMeta,
pack_metadata: &ModpackMeta,
) -> 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 {
mod_providers
} else {
@ -52,28 +86,35 @@ impl PinnedPackMeta {
match mod_provider {
crate::mod_meta::ModProvider::CurseForge => unimplemented!(),
crate::mod_meta::ModProvider::Modrinth => {
let pinned_mods = self.modrinth.resolve(&mod_metadata, pack_metadata).await;
if let Ok(pinned_mods) = pinned_mods {
pinned_mods.into_iter().for_each(|m| {
self.mods.insert(mod_metadata.name.clone(), m);
});
} else if let Err(e) = pinned_mods {
return Err(format!(
"Failed to resolve mod '{}' (provider={:#?}) with constraint {}: {}",
mod_metadata.name, mod_provider, mod_metadata.version, e
)
.into());
let pinned_mod = self.modrinth.resolve(&mod_metadata, pack_metadata).await;
if let Ok(pinned_mod) = pinned_mod {
self.mods
.insert(mod_metadata.name.clone(), pinned_mod.clone());
println!("Pinned {}@{}", mod_metadata.name, pinned_mod.version);
if let Some(deps) = &pinned_mod.deps {
return Ok(deps
.iter()
.filter(|d| !self.mods.contains_key(&d.name))
.cloned()
.collect());
}
return Ok(vec![]);
}
}
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>> {
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(())
}
@ -88,8 +129,12 @@ impl PinnedPackMeta {
}
pub fn save_current_dir_lock(&self) -> Result<(), Box<dyn Error>> {
let modpack_lock_file_path =
std::env::current_dir()?.join(PathBuf::from(MODPACK_LOCK_FILENAME));
self.save_to_dir(&std::env::current_dir()?)?;
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)?;
Ok(())
}