1use crate::pkg::{config, install, resolve, transaction, types};
2use crate::project;
3use colored::Colorize;
4use rayon::prelude::*;
5use std::collections::HashSet;
6use std::sync::Mutex;
7
8pub fn run(
9 sources: &[String],
10 repo: Option<String>,
11 force: bool,
12 all_optional: bool,
13 yes: bool,
14 scope: Option<crate::cli::InstallScope>,
15 local: bool,
16 global: bool,
17 save: bool,
18) {
19 let mut scope_override = scope.map(|s| match s {
20 crate::cli::InstallScope::User => types::Scope::User,
21 crate::cli::InstallScope::System => types::Scope::System,
22 crate::cli::InstallScope::Project => types::Scope::Project,
23 });
24
25 if local {
26 scope_override = Some(types::Scope::Project);
27 } else if global {
28 scope_override = Some(types::Scope::User);
29 }
30
31 if sources.is_empty()
32 && repo.is_none()
33 && let Ok(config) = project::config::load()
34 && config.config.local
35 {
36 let old_lockfile = project::lockfile::read_zoi_lock().ok();
37
38 println!("Installing project packages locally...");
39 let local_scope = Some(types::Scope::Project);
40 let failed_packages = Mutex::new(Vec::new());
41 let processed_deps = Mutex::new(HashSet::new());
42 let installed_packages_info = Mutex::new(Vec::new());
43
44 config.pkgs.par_iter().for_each(|source| {
45 println!("=> Installing package: {}", source.cyan().bold());
46 if let Err(e) = install::run_installation(
47 source,
48 install::InstallMode::PreferPrebuilt,
49 force,
50 types::InstallReason::Direct,
51 yes,
52 all_optional,
53 &processed_deps,
54 local_scope,
55 None,
56 ) {
57 eprintln!(
58 "{}: Failed to install '{}': {}",
59 "Error".red().bold(),
60 source,
61 e
62 );
63 failed_packages.lock().unwrap().push(source.to_string());
64 } else if let Ok((pkg, _, _, _, registry_handle)) =
65 resolve::resolve_package_and_version(source)
66 {
67 installed_packages_info
68 .lock()
69 .unwrap()
70 .push((pkg, registry_handle));
71 }
72 });
73
74 let failed_packages = failed_packages.into_inner().unwrap();
75 if !failed_packages.is_empty() {
76 eprintln!(
77 "\n{}: The following packages failed to install:",
78 "Error".red().bold()
79 );
80 for pkg in &failed_packages {
81 eprintln!(" - {}", pkg);
82 }
83 std::process::exit(1);
84 }
85
86 let mut new_lockfile_packages = std::collections::HashMap::new();
87 let installed_packages_info = installed_packages_info.into_inner().unwrap();
88 for (pkg, registry_handle) in &installed_packages_info {
89 let handle = registry_handle.as_deref().unwrap_or("local");
90 if let Ok(package_dir) = crate::pkg::local::get_package_dir(
91 types::Scope::Project,
92 handle,
93 &pkg.repo,
94 &pkg.name,
95 ) {
96 let latest_dir = package_dir.join("latest");
97 if let Ok(hash) = crate::pkg::hash::calculate_dir_hash(&latest_dir) {
98 new_lockfile_packages.insert(pkg.name.clone(), hash);
99 }
100 }
101 }
102
103 if let Some(old_lock) = old_lockfile {
104 for (pkg_name, new_hash) in &new_lockfile_packages {
105 if let Some(old_hash) = old_lock.packages.get(pkg_name)
106 && old_hash != new_hash
107 {
108 println!("Warning: Hash mismatch for package '{}'.", pkg_name);
109 }
110 }
111 }
112
113 let new_lockfile = types::ZoiLock {
114 packages: new_lockfile_packages,
115 };
116 if let Err(e) = project::lockfile::write_zoi_lock(&new_lockfile) {
117 eprintln!("Warning: Failed to write zoi.lock file: {}", e);
118 }
119
120 return;
121 }
122
123 if scope_override.is_none()
124 && let Ok(config) = project::config::load()
125 && config.config.local
126 {
127 scope_override = Some(types::Scope::Project);
128 }
129
130 if let Some(repo_spec) = repo {
131 if scope_override == Some(types::Scope::Project) {
132 eprintln!(
133 "{}: Installing from a repository to a project scope is not supported.",
134 "Error".red().bold()
135 );
136 std::process::exit(1);
137 }
138 let repo_install_scope = scope_override.map(|s| match s {
139 types::Scope::User => crate::cli::SetupScope::User,
140 types::Scope::System => crate::cli::SetupScope::System,
141 types::Scope::Project => unreachable!(),
142 });
143
144 if let Err(e) =
145 crate::pkg::repo_install::run(&repo_spec, force, all_optional, yes, repo_install_scope)
146 {
147 eprintln!(
148 "{}: Failed to install from repo '{}': {}",
149 "Error".red().bold(),
150 repo_spec,
151 e
152 );
153 std::process::exit(1);
154 }
155 return;
156 }
157
158 let config = config::read_config().unwrap_or_default();
159
160 let parallel_jobs = config.parallel_jobs.unwrap_or(3);
161
162 if parallel_jobs > 0 {
163 rayon::ThreadPoolBuilder::new()
164 .num_threads(parallel_jobs)
165 .build_global()
166 .unwrap();
167 }
168
169 let mode = install::InstallMode::PreferPrebuilt;
170
171 let failed_packages = Mutex::new(Vec::new());
172
173 let mut temp_files = Vec::new();
174
175 let mut sources_to_process: Vec<String> = Vec::new();
176
177 for source in sources {
178 if source.ends_with("zoi.pkgs.json") {
179 if let Err(e) = install::lockfile::process_lockfile(
180 source,
181 &mut sources_to_process,
182 &mut temp_files,
183 ) {
184 eprintln!(
185 "{}: Failed to process lockfile '{}': {}",
186 "Error".red().bold(),
187 source,
188 e
189 );
190
191 failed_packages.lock().unwrap().push(source.to_string());
192 }
193 } else {
194 sources_to_process.push(source.to_string());
195 }
196 }
197
198 let successfully_installed_sources = Mutex::new(Vec::new());
199
200 println!("{}", "Resolving dependencies...".bold());
201
202 let graph = match install::resolver::resolve_dependency_graph(
203 &sources_to_process,
204 scope_override,
205 force,
206 yes,
207 all_optional,
208 ) {
209 Ok(g) => g,
210
211 Err(e) => {
212 eprintln!("{}: {}", "Failed to resolve dependencies".red().bold(), e);
213
214 std::process::exit(1);
215 }
216 };
217
218 let stages = match graph.toposort() {
219 Ok(s) => s,
220
221 Err(e) => {
222 eprintln!("{}: {}", "Failed to sort dependencies".red().bold(), e);
223
224 std::process::exit(1);
225 }
226 };
227
228 let transaction = match transaction::begin() {
229 Ok(t) => t,
230 Err(e) => {
231 eprintln!("Failed to begin transaction: {}", e);
232 std::process::exit(1);
233 }
234 };
235
236 println!("\nStarting installation...");
237 let mut overall_success = true;
238
239 for (i, stage) in stages.iter().enumerate() {
240 println!(
241 "--- Installing Stage {}/{} ({} packages) ---",
242 i + 1,
243 stages.len(),
244 stage.len()
245 );
246
247 stage.par_iter().for_each(|pkg_id| {
248 let node = graph.nodes.get(pkg_id).unwrap();
249
250 println!("Installing {}...", node.pkg.name.cyan());
251
252 match install::installer::install_node(node, mode, None) {
253 Ok(manifest) => {
254 println!("Successfully installed {}", node.pkg.name.green());
255
256 if let Err(e) = transaction::record_operation(
257 &transaction.id,
258 types::TransactionOperation::Install {
259 manifest: Box::new(manifest),
260 },
261 ) {
262 eprintln!(
263 "Error: Failed to record transaction operation for {}: {}",
264 node.pkg.name, e
265 );
266 failed_packages.lock().unwrap().push(node.pkg.name.clone());
267 }
268
269 if matches!(node.reason, types::InstallReason::Direct) {
270 successfully_installed_sources
271 .lock()
272 .unwrap()
273 .push(node.source.clone());
274 }
275 }
276
277 Err(e) => {
278 eprintln!(
279 "{}: Failed to install {}: {}",
280 "Error".red().bold(),
281 node.pkg.name,
282 e
283 );
284
285 failed_packages.lock().unwrap().push(node.pkg.name.clone());
286 }
287 }
288 });
289
290 let failed = failed_packages.lock().unwrap();
291
292 if !failed.is_empty() {
293 eprintln!(
294 "\n{}: Installation failed at stage {}.",
295 "Error".red().bold(),
296 i + 1
297 );
298 overall_success = false;
299 break;
300 }
301 }
302
303 if !overall_success {
304 eprintln!(
305 "\n{}: The following packages failed to install:",
306 "Error".red().bold()
307 );
308 for pkg in &failed_packages.into_inner().unwrap() {
309 eprintln!(" - {}", pkg);
310 }
311
312 eprintln!("\n{} Rolling back changes...", "---".yellow().bold());
313 if let Err(e) = transaction::rollback(&transaction.id) {
314 eprintln!("\nCRITICAL: Rollback failed: {}", e);
315 eprintln!(
316 "The system may be in an inconsistent state. The transaction log is at ~/.zoi/transactions/{}.json",
317 transaction.id
318 );
319 } else {
320 println!("\n{} Rollback successful.", "Success:".green().bold());
321 }
322
323 std::process::exit(1);
324 }
325
326 if let Err(e) = transaction::commit(&transaction.id) {
327 eprintln!("Warning: Failed to commit transaction: {}", e);
328 }
329
330 if save && scope_override == Some(types::Scope::Project) {
331 let successfully_installed = successfully_installed_sources.into_inner().unwrap();
332
333 if !successfully_installed.is_empty()
334 && let Err(e) = project::config::add_packages_to_config(&successfully_installed)
335 {
336 eprintln!(
337 "{}: Failed to save packages to zoi.yaml: {}",
338 "Warning".yellow().bold(),
339 e
340 );
341 }
342 }
343
344 println!("\n{} Installation complete!", "Success:".green().bold());
345}