initial commit

This commit is contained in:
2026-02-26 17:08:38 +01:00
commit c34a957a9d
8 changed files with 854 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
/data

92
Cargo.lock generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
}
}