conduit_cli/core/installer/
project.rs1use 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}