Skip to main content

conduit_cli/core/installer/
project.rs

1use crate::config::ConduitConfig;
2use crate::core::error::CoreResult;
3use crate::core::installer::extra_deps::{ExtraDepsPolicy, InstallerUi};
4use crate::core::installer::resolve::{install_mod, InstallOptions};
5use crate::core::installer::sync::sync_from_lock;
6use crate::core::io::{load_config, load_lock, save_config, save_lock};
7use crate::core::paths::CorePaths;
8use crate::lock::ConduitLock;
9use crate::modrinth::ModrinthAPI;
10use std::collections::HashSet;
11use std::fs;
12
13pub struct InstallProjectOptions {
14    pub extra_deps_policy: ExtraDepsPolicy,
15    pub strict: bool,
16    pub force: bool,
17}
18
19#[derive(Debug, Default, Clone)]
20pub struct SyncProjectReport {
21    pub pruned_files: Vec<String>,
22}
23
24impl Default for InstallProjectOptions {
25    fn default() -> Self {
26        Self {
27            extra_deps_policy: ExtraDepsPolicy::Skip,
28            strict: false,
29            force: false,
30        }
31    }
32}
33
34pub async fn add_mod_to_project(
35    api: &ModrinthAPI,
36    paths: &CorePaths,
37    input: &str,
38    ui: &mut dyn InstallerUi,
39    options: InstallProjectOptions,
40) -> CoreResult<()> {
41    let mut config: ConduitConfig = load_config(paths)?;
42    let mut lock = load_lock(paths)?;
43
44    install_mod(
45        api,
46        paths,
47        input,
48        &mut config,
49        &mut lock,
50        ui,
51        InstallOptions {
52            is_root: true,
53            extra_deps_policy: options.extra_deps_policy,
54        },
55    )
56    .await?;
57
58    save_config(paths, &config)?;
59    save_lock(paths, &lock)?;
60    Ok(())
61}
62
63pub async fn sync_project(
64    api: &ModrinthAPI,
65    paths: &CorePaths,
66    ui: &mut dyn InstallerUi,
67    options: InstallProjectOptions,
68) -> CoreResult<SyncProjectReport> {
69    let mut config: ConduitConfig = load_config(paths)?;
70    let mut lock = load_lock(paths)?;
71
72    if options.force {
73        lock = rebuild_lock_from_config(api, paths, ui, &config, &lock, &options).await?;
74    }
75
76    let mods_to_check: Vec<String> = config
77        .mods
78        .iter()
79        .filter(|(_k, v)| v != &"local")
80        .map(|(k, _v)| k.clone())
81        .collect();
82    for slug in mods_to_check {
83        if !lock.locked_mods.contains_key(&slug) {
84            let input = if let Some(version) = config.mods.get(&slug) {
85                if version != "latest" {
86                    format!("{}@{}", slug, version)
87                } else {
88                    slug.clone()
89                }
90            } else {
91                slug.clone()
92            };
93
94            install_mod(
95                api,
96                paths,
97                &input,
98                &mut config,
99                &mut lock,
100                ui,
101                InstallOptions {
102                    is_root: true,
103                    extra_deps_policy: options.extra_deps_policy.clone(),
104                },
105            )
106            .await?;
107        }
108    }
109
110    sync_from_lock(
111        paths,
112        lock.locked_mods
113            .values()
114            .filter(|m| m.url != "local"),
115        ui,
116    )
117    .await?;
118
119    let mut report = SyncProjectReport::default();
120    if options.strict {
121        report.pruned_files = prune_unmanaged_mods(paths, &config, &lock)?;
122    }
123
124    save_config(paths, &config)?;
125    save_lock(paths, &lock)?;
126
127    Ok(report)
128}
129
130async fn rebuild_lock_from_config(
131    api: &ModrinthAPI,
132    paths: &CorePaths,
133    ui: &mut dyn InstallerUi,
134    config: &ConduitConfig,
135    existing_lock: &ConduitLock,
136    options: &InstallProjectOptions,
137) -> CoreResult<ConduitLock> {
138    let mut new_lock = ConduitLock {
139        version: existing_lock.version,
140        locked_mods: existing_lock
141            .locked_mods
142            .iter()
143            .filter(|(_k, v)| v.url == "local")
144            .map(|(k, v)| (k.clone(), crate::lock::LockedMod {
145                id: v.id.clone(),
146                version_id: v.version_id.clone(),
147                filename: v.filename.clone(),
148                url: v.url.clone(),
149                hash: v.hash.clone(),
150                dependencies: v.dependencies.clone(),
151            }))
152            .collect(),
153    };
154
155    let mut dummy_config = config.clone();
156
157    for (slug, version) in &config.mods {
158        if version == "local" {
159            continue;
160        }
161
162        let input = if version != "latest" {
163            format!("{}@{}", slug, version)
164        } else {
165            slug.clone()
166        };
167
168        install_mod(
169            api,
170            paths,
171            &input,
172            &mut dummy_config,
173            &mut new_lock,
174            ui,
175            InstallOptions {
176                is_root: false,
177                extra_deps_policy: options.extra_deps_policy.clone(),
178            },
179        )
180        .await?;
181    }
182
183    Ok(new_lock)
184}
185
186fn prune_unmanaged_mods(
187    paths: &CorePaths,
188    config: &ConduitConfig,
189    lock: &ConduitLock,
190) -> CoreResult<Vec<String>> {
191    let mut managed_files: HashSet<String> = HashSet::new();
192
193    for (key, m) in &lock.locked_mods {
194        if config.mods.contains_key(key) || m.url != "local" {
195            managed_files.insert(m.filename.clone());
196        }
197    }
198
199    let mut pruned = Vec::new();
200    if let Ok(read_dir) = fs::read_dir(paths.mods_dir()) {
201        for entry in read_dir {
202            let entry = entry?;
203            let path = entry.path();
204            if path.extension().and_then(|e| e.to_str()) != Some("jar") {
205                continue;
206            }
207            let filename = match path.file_name().and_then(|n| n.to_str()) {
208                Some(f) => f.to_string(),
209                None => continue,
210            };
211            if managed_files.contains(&filename) {
212                continue;
213            }
214            fs::remove_file(&path)?;
215            pruned.push(filename);
216        }
217    }
218
219    Ok(pruned)
220}