initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/data
|
||||
92
Cargo.lock
generated
Normal file
92
Cargo.lock
generated
Normal file
@@ -0,0 +1,92 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "fomod-manager"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.39.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "fomod-manager"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
quick-xml = { version = "0.39.2", features = ["serde-types", "serialize"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
314
src/fomod.rs
Normal file
314
src/fomod.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
// This file incorporates code from luctius/fomod:
|
||||
// https://github.com/luctius/fomod/
|
||||
// Original license: MIT / Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct Info {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: Option<String>,
|
||||
#[serde(rename = "Description")]
|
||||
pub description: Option<String>,
|
||||
#[serde(rename = "Version")]
|
||||
pub version: Option<String>,
|
||||
#[serde(rename = "Author")]
|
||||
pub author: Option<String>,
|
||||
#[serde(rename = "Website")]
|
||||
pub website: Option<String>,
|
||||
#[serde(rename = "CategoryId")]
|
||||
pub category_id: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct Config {
|
||||
#[serde(rename = "moduleName")]
|
||||
pub module_name: String,
|
||||
|
||||
#[serde(rename = "moduleImage")]
|
||||
pub module_image: Option<HeaderImage>,
|
||||
|
||||
#[serde(rename = "moduleDependencies")]
|
||||
pub module_dependencies: Option<ModuleDependency>,
|
||||
|
||||
#[serde(rename = "requiredInstallFiles")]
|
||||
pub required_install_files: Option<FileList>,
|
||||
|
||||
#[serde(rename = "installSteps")]
|
||||
pub install_steps: Option<StepList>,
|
||||
|
||||
#[serde(rename = "conditionalFileInstalls")]
|
||||
pub conditional_file_installs: Option<ConditionalFileInstallList>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
|
||||
)]
|
||||
pub enum OrderEnum {
|
||||
#[default]
|
||||
Ascending,
|
||||
Explicit,
|
||||
Descending,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum PluginTypeEnum {
|
||||
Required,
|
||||
Optional,
|
||||
Recommended,
|
||||
NotUsable,
|
||||
CouldBeUsable,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct PluginType {
|
||||
#[serde(rename = "@name")]
|
||||
pub name: PluginTypeEnum,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct PluginTypeDescriptor {
|
||||
#[serde(rename = "$value")]
|
||||
pub value: PluginTypeDescriptorEnum,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum PluginTypeDescriptorEnum {
|
||||
#[serde(rename = "dependencyType")]
|
||||
DependencyType(DependencyPluginType),
|
||||
#[serde(rename = "type")]
|
||||
PluginType(PluginType),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct DependencyPluginType {
|
||||
pub default_type: PluginType,
|
||||
pub patterns: DependencyPatternList,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct DependencyPatternList {
|
||||
pub pattern: Vec<DependencyPattern>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct DependencyPattern {
|
||||
pub dependencies: CompositeDependency,
|
||||
#[serde(rename = "type")]
|
||||
pub typ: PluginType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct StepList {
|
||||
#[serde(rename = "@order", default)]
|
||||
pub order: OrderEnum,
|
||||
|
||||
#[serde(rename = "installStep")]
|
||||
pub install_step: Vec<InstallStep>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct InstallStep {
|
||||
#[serde(rename = "@name")]
|
||||
pub name: String,
|
||||
|
||||
pub visible: Option<CompositeDependency>,
|
||||
|
||||
#[serde(rename = "optionalFileGroups")]
|
||||
pub optional_file_groups: GroupList,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ModuleDependency {
|
||||
#[serde(rename = "@operator")]
|
||||
pub operator: DependencyOperator,
|
||||
#[serde(rename = "$value")]
|
||||
pub list: Vec<CompositeDependency>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum CompositeDependency {
|
||||
#[serde(rename = "fileDependency")]
|
||||
File(FileDependency),
|
||||
#[serde(rename = "flagDependency")]
|
||||
Flag(FlagDependency),
|
||||
#[serde(rename = "gameDependency")]
|
||||
Game(VersionDependency),
|
||||
#[serde(rename = "fommDependency")]
|
||||
Fomm(VersionDependency),
|
||||
#[serde(rename = "dependencies")]
|
||||
Dependency(ModuleDependency),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FlagDependency {
|
||||
#[serde(rename = "@flag")]
|
||||
pub flag: String,
|
||||
#[serde(rename = "@value")]
|
||||
pub value: String,
|
||||
}
|
||||
impl From<SetConditionFlag> for FlagDependency {
|
||||
fn from(scf: SetConditionFlag) -> Self {
|
||||
Self {
|
||||
flag: scf.name,
|
||||
value: scf.flag_value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct VersionDependency {
|
||||
#[serde(rename = "@version")]
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FileDependency {
|
||||
#[serde(rename = "@file")]
|
||||
pub file_name: String,
|
||||
#[serde(rename = "@state")]
|
||||
pub state: DependencyState,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum DependencyState {
|
||||
Active,
|
||||
Inactive,
|
||||
Missing,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum DependencyOperator {
|
||||
And,
|
||||
Or,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FileList {
|
||||
#[serde(rename = "$value")]
|
||||
pub list: Option<Vec<FileTypeEnum>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum FileTypeEnum {
|
||||
#[serde(rename = "file")]
|
||||
File(FileType),
|
||||
#[serde(rename = "folder")]
|
||||
Folder(FileType),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FileType {
|
||||
#[serde(rename = "@source")]
|
||||
pub source: String,
|
||||
#[serde(rename = "@destination")]
|
||||
pub destination: Option<String>,
|
||||
#[serde(rename = "@alwaysInstall")]
|
||||
pub always_install: Option<String>,
|
||||
#[serde(rename = "@installIfUsable", default = "false_bool")]
|
||||
pub install_if_usable: bool,
|
||||
pub priority: Option<isize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct GroupList {
|
||||
#[serde(rename = "@order", default)]
|
||||
pub order: OrderEnum,
|
||||
pub group: Vec<Group>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Group {
|
||||
#[serde(rename = "@name")]
|
||||
pub name: String,
|
||||
|
||||
#[serde(rename = "@type")]
|
||||
pub typ: GroupType,
|
||||
|
||||
pub plugins: PluginList,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum GroupType {
|
||||
SelectAtLeastOne,
|
||||
SelectAtMostOne,
|
||||
SelectExactlyOne,
|
||||
SelectAll,
|
||||
SelectAny,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct PluginList {
|
||||
#[serde(rename = "@order", default)]
|
||||
pub order: OrderEnum,
|
||||
|
||||
pub plugin: Vec<Plugin>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Plugin {
|
||||
#[serde(rename = "@name")]
|
||||
pub name: String,
|
||||
|
||||
pub description: String,
|
||||
|
||||
pub image: Option<Image>,
|
||||
|
||||
pub files: Option<FileList>,
|
||||
#[serde(rename = "conditionFlags")]
|
||||
pub condition_flags: Option<ConditionFlagList>,
|
||||
|
||||
#[serde(rename = "typeDescriptor")]
|
||||
pub type_descriptor: Option<PluginTypeDescriptor>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Image {
|
||||
#[serde(rename = "@path")]
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct HeaderImage {
|
||||
#[serde(rename = "@path")]
|
||||
pub path: Option<String>,
|
||||
#[serde(rename = "@showImage", default = "false_bool")]
|
||||
pub show_image: bool,
|
||||
#[serde(rename = "@showFade", default = "false_bool")]
|
||||
pub show_fade: bool,
|
||||
pub height: Option<isize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ConditionFlagList {
|
||||
pub flag: Vec<SetConditionFlag>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct SetConditionFlag {
|
||||
#[serde(rename = "@name")]
|
||||
pub name: String,
|
||||
|
||||
#[serde(rename = "$value")]
|
||||
pub flag_value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ConditionalFileInstallList {
|
||||
pub patterns: ConditionalInstallPatternList,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ConditionalInstallPatternList {
|
||||
pub pattern: Vec<ConditionalInstallPattern>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ConditionalInstallPattern {
|
||||
pub dependencies: CompositeDependency,
|
||||
pub files: FileList,
|
||||
}
|
||||
|
||||
fn false_bool() -> bool {
|
||||
false
|
||||
}
|
||||
61
src/install_prompt.rs
Normal file
61
src/install_prompt.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crate::{fomod::GroupType, mod_config_installer::GroupPrompt};
|
||||
|
||||
pub fn prompt(p: GroupPrompt) -> Vec<usize> {
|
||||
println!("=== {} ===", p.name);
|
||||
println!();
|
||||
|
||||
for option in &p.options {
|
||||
println!(
|
||||
"[{}] {} ({:?}) => {}",
|
||||
option.idx + 1, option.name, option.option_type, option.description
|
||||
);
|
||||
}
|
||||
|
||||
let instruction = match p.select_type {
|
||||
GroupType::SelectExactlyOne => "Select one (enter number)",
|
||||
GroupType::SelectAtLeastOne => "Select one or more (e.g. 1 2 3)",
|
||||
GroupType::SelectAtMostOne => "Select one or none (enter number or 0 for none)",
|
||||
GroupType::SelectAny => "Select any (e.g. 1 2 3, or 0 for none)",
|
||||
GroupType::SelectAll => {
|
||||
println!(" (all options required)");
|
||||
return (0..p.options.len()).collect();
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
println!("=> {}", instruction);
|
||||
io::stdout().flush().unwrap();
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input).unwrap();
|
||||
input.trim().to_string();
|
||||
|
||||
let choices: Vec<usize> = input
|
||||
.split_whitespace()
|
||||
.filter_map(|s| s.parse::<usize>().ok())
|
||||
.filter(|&n| n > 0 && n <= p.options.len())
|
||||
.map(|n| n - 1)
|
||||
.collect();
|
||||
|
||||
let valid = match p.select_type {
|
||||
GroupType::SelectExactlyOne => {
|
||||
choices.len() == 1 // TODO: Check not usable
|
||||
}
|
||||
GroupType::SelectAtLeastOne => {
|
||||
!choices.is_empty() // TODO: Check not usable
|
||||
}
|
||||
GroupType::SelectAtMostOne => {
|
||||
choices.len() <= 1 // TODO: Check not usable
|
||||
}
|
||||
GroupType::SelectAny => true, // TODO: Check selected not usable,
|
||||
GroupType::SelectAll => unreachable!(),
|
||||
};
|
||||
|
||||
if valid {
|
||||
// TODO: Merge required plugins into final selection
|
||||
let final_selection = choices;
|
||||
return final_selection;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/linker.rs
Normal file
111
src/linker.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use std::{
|
||||
fs, io,
|
||||
os::unix,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub struct Linker {
|
||||
target: PathBuf,
|
||||
}
|
||||
|
||||
impl Linker {
|
||||
pub fn new(target_path: &Path) -> Self {
|
||||
Self {
|
||||
target: target_path.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn install_path(&self) -> &Path {
|
||||
&self.target
|
||||
}
|
||||
|
||||
fn link_file(&self, from: &Path, to: &Path) -> Result<(), LinkerError> {
|
||||
let target = self.install_path().join(to);
|
||||
|
||||
if let Some(parent) = target.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
unix::fs::symlink(from, target)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_link(&self, to: &Path) -> Result<(), LinkerError> {
|
||||
let file = self.install_path().join(to);
|
||||
let metadata = fs::symlink_metadata(&file)?;
|
||||
|
||||
if !metadata.file_type().is_symlink() {
|
||||
return Err(LinkerError::NotASymlink(file));
|
||||
}
|
||||
|
||||
fs::remove_file(&file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn link_recursive(&self, from: &Path, to: &Path) -> Result<(), LinkerError> {
|
||||
for entry in fs::read_dir(from)? {
|
||||
let entry = entry?;
|
||||
let entry_path = entry.path();
|
||||
let relative = entry_path
|
||||
.strip_prefix(from)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
|
||||
let target_path = to.join(relative);
|
||||
|
||||
if entry_path.is_dir() {
|
||||
self.link_recursive(&entry_path, &target_path)?;
|
||||
} else {
|
||||
self.link_file(&entry_path, &target_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unlink_recursive(&self, from: &Path, to: &Path) -> Result<(), LinkerError> {
|
||||
for entry in fs::read_dir(from)? {
|
||||
let entry = entry?;
|
||||
let entry_path = entry.path();
|
||||
let relative = entry_path
|
||||
.strip_prefix(from)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
|
||||
let target_path = to.join(relative);
|
||||
|
||||
if entry_path.is_dir() {
|
||||
self.unlink_recursive(&entry_path, &target_path)?;
|
||||
|
||||
let install_target = self.install_path().join(&target_path);
|
||||
if install_target.exists() && fs::read_dir(&install_target)?.next().is_none() {
|
||||
fs::remove_dir(&install_target)?;
|
||||
}
|
||||
} else {
|
||||
self.remove_link(&target_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LinkerError {
|
||||
Io(io::Error),
|
||||
NotASymlink(PathBuf),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LinkerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Io(e) => write!(f, "IO error: {}", e),
|
||||
Self::NotASymlink(path) => write!(f, "Tried to remove a non symlink: {:?}", path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LinkerError {}
|
||||
|
||||
impl From<io::Error> for LinkerError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Self::Io(e)
|
||||
}
|
||||
}
|
||||
22
src/main.rs
Normal file
22
src/main.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use std::{error::Error, fs};
|
||||
|
||||
use crate::mod_config_installer::FomodInstaller;
|
||||
|
||||
mod install_prompt;
|
||||
mod mod_config_installer;
|
||||
mod linker;
|
||||
mod fomod;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
const XML_PATH: &str = "./data/xml/ineed.xml";
|
||||
|
||||
let xml = fs::read_to_string(XML_PATH)?;
|
||||
|
||||
let config: fomod::Config = quick_xml::de::from_str(&xml)?;
|
||||
|
||||
let installer = FomodInstaller::new(config, vec![], install_prompt::prompt);
|
||||
|
||||
dbg!(installer.run());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
244
src/mod_config_installer.rs
Normal file
244
src/mod_config_installer.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use std::{collections::HashMap, fmt::Display};
|
||||
|
||||
use crate::fomod::{
|
||||
CompositeDependency, Config, DependencyOperator, DependencyState, FileList, FileTypeEnum,
|
||||
Group, GroupType, Plugin, PluginTypeDescriptorEnum, PluginTypeEnum,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InstallerState {
|
||||
/// Flags set by plugin selections throughout the install
|
||||
flags: HashMap<String, String>,
|
||||
|
||||
/// Files to install, keyed by destination path.
|
||||
/// Higher priority value wins when destinations conflict.
|
||||
selected_files: Vec<FileTypeEnum>,
|
||||
}
|
||||
|
||||
impl InstallerState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
flags: HashMap::new(),
|
||||
selected_files: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_flag(&mut self, name: &str, value: &str) {
|
||||
self.flags.insert(name.to_string(), value.to_string());
|
||||
}
|
||||
|
||||
fn get_flag(&self, name: &str) -> Option<&str> {
|
||||
self.flags.get(name).map(|s| s.as_str())
|
||||
}
|
||||
|
||||
fn add_files(&mut self, files: &FileList) {
|
||||
let Some(list) = &files.list else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.selected_files.extend_from_slice(list);
|
||||
}
|
||||
|
||||
pub fn into_file_list(self) -> Vec<FileTypeEnum> {
|
||||
self.selected_files
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_dependency(
|
||||
dep: &CompositeDependency,
|
||||
state: &InstallerState,
|
||||
installed_plugins: &[String],
|
||||
) -> bool {
|
||||
match dep {
|
||||
CompositeDependency::Flag(flag) => state
|
||||
.get_flag(&flag.flag)
|
||||
.map(|v| v == flag.value)
|
||||
.unwrap_or(false),
|
||||
|
||||
CompositeDependency::File(file) => {
|
||||
let exists = installed_plugins
|
||||
.iter()
|
||||
.any(|p| p.eq_ignore_ascii_case(&file.file_name));
|
||||
|
||||
match file.state {
|
||||
DependencyState::Active => exists,
|
||||
DependencyState::Inactive => !exists,
|
||||
DependencyState::Missing => !exists,
|
||||
}
|
||||
}
|
||||
|
||||
CompositeDependency::Game(version) => {
|
||||
// TODO: Check the game version
|
||||
let _ = version;
|
||||
true
|
||||
}
|
||||
|
||||
CompositeDependency::Fomm(version) => {
|
||||
// TODO: Check the fomm version
|
||||
let _ = version;
|
||||
true
|
||||
}
|
||||
|
||||
CompositeDependency::Dependency(module) => {
|
||||
let mut results = module
|
||||
.list
|
||||
.iter()
|
||||
.map(|dep| evaluate_dependency(dep, state, installed_plugins));
|
||||
|
||||
match module.operator {
|
||||
DependencyOperator::And => results.all(|r| r),
|
||||
DependencyOperator::Or => results.any(|r| r),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GroupPrompt {
|
||||
pub name: String,
|
||||
pub select_type: GroupType,
|
||||
pub options: Vec<InstallOption>,
|
||||
}
|
||||
|
||||
pub struct InstallOption {
|
||||
pub name: String,
|
||||
pub option_type: PluginTypeEnum,
|
||||
pub idx: usize,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl Display for InstallOption {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} ({:?})", self.name, self.option_type)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_group_prompt(
|
||||
group: &Group,
|
||||
state: &InstallerState,
|
||||
installed_plugins: &[String],
|
||||
) -> GroupPrompt {
|
||||
let options = group
|
||||
.plugins
|
||||
.plugin
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, option)| InstallOption {
|
||||
name: option.name.trim().to_owned(),
|
||||
option_type: resolve_plugin_type(option, state, installed_plugins),
|
||||
description: option.description.trim().to_owned(),
|
||||
idx,
|
||||
})
|
||||
.collect();
|
||||
|
||||
GroupPrompt {
|
||||
name: group.name.clone(),
|
||||
select_type: group.typ,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
/// Each plugin in the install steps have a type. e.g.: Optional, Required, Recommended
|
||||
/// But the type depends on dependencies
|
||||
/// This function eveluates the plugin type
|
||||
fn resolve_plugin_type(
|
||||
plugin: &Plugin,
|
||||
state: &InstallerState,
|
||||
installed_plugins: &[String],
|
||||
) -> PluginTypeEnum {
|
||||
// Since the type_descriptor is optional return Optional if non is set
|
||||
let Some(descriptor) = &plugin.type_descriptor else {
|
||||
return PluginTypeEnum::Optional;
|
||||
};
|
||||
|
||||
match &descriptor.value {
|
||||
PluginTypeDescriptorEnum::PluginType(plugin_type) => plugin_type.name,
|
||||
PluginTypeDescriptorEnum::DependencyType(dependency_plugin_type) => {
|
||||
for dep in &dependency_plugin_type.patterns.pattern {
|
||||
if evaluate_dependency(&dep.dependencies, state, installed_plugins) {
|
||||
return dep.typ.name;
|
||||
}
|
||||
}
|
||||
|
||||
// No dependencies was satisfied. Using default type
|
||||
dependency_plugin_type.default_type.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FomodInstaller {
|
||||
config: Config,
|
||||
installed_plugins: Vec<String>,
|
||||
group_prompt: fn(GroupPrompt) -> Vec<usize>,
|
||||
}
|
||||
|
||||
impl FomodInstaller {
|
||||
pub fn new(
|
||||
config: Config,
|
||||
installed_plugins: Vec<String>,
|
||||
group_promt: fn(GroupPrompt) -> Vec<usize>,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
installed_plugins,
|
||||
group_prompt: group_promt,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(self) -> Vec<FileTypeEnum> {
|
||||
let mut state = InstallerState::new();
|
||||
|
||||
// Always-installed files first
|
||||
if let Some(required) = &self.config.required_install_files {
|
||||
state.add_files(required);
|
||||
}
|
||||
|
||||
if let Some(install_steps) = &self.config.install_steps {
|
||||
let steps = &install_steps.install_step;
|
||||
|
||||
for (step_index, step) in steps.iter().enumerate() {
|
||||
// Check if the step should be visible
|
||||
if let Some(visible) = &step.visible {
|
||||
if !evaluate_dependency(visible, &state, &self.installed_plugins) {
|
||||
// Dependency to show the step not meet. Skipping.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for group in &step.optional_file_groups.group {
|
||||
// TODO: Skip groups where all plugins are NotUsable
|
||||
|
||||
let prompt = create_group_prompt(group, &state, &self.installed_plugins);
|
||||
|
||||
let selected_plugins = (self.group_prompt)(prompt);
|
||||
|
||||
for i in selected_plugins {
|
||||
let plugin = &group.plugins.plugin[i];
|
||||
|
||||
// Add files from selected plugin
|
||||
if let Some(files) = &plugin.files {
|
||||
state.add_files(files);
|
||||
}
|
||||
|
||||
// Set condition flags
|
||||
if let Some(condition_flags) = &plugin.condition_flags {
|
||||
for flag in &condition_flags.flag {
|
||||
state.set_flag(&flag.name, &flag.flag_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate conditional file installs based on final flag state
|
||||
if let Some(conditional) = &self.config.conditional_file_installs {
|
||||
for pattern in &conditional.patterns.pattern {
|
||||
if evaluate_dependency(&pattern.dependencies, &state, &self.installed_plugins) {
|
||||
state.add_files(&pattern.files);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.into_file_list()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user