zoi/cmd/
install.rs

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}