Skip to main content

zoi_cli/cmd/
dev.rs

1//! Implementation of the `dev` command for project development environments.
2
3use std::collections::HashMap;
4use std::process::Command;
5
6use anyhow::{Result, anyhow};
7use colored::Colorize;
8use indicatif::MultiProgress;
9use zoi_project::config as project_config;
10
11use crate::pkg::{install, local, resolve, types};
12
13/// Runs the `dev` command to enter a development shell for a project.
14/// # Errors
15///
16/// Returns an error if the development shell cannot be entered or dependencies
17/// cannot be resolved.
18pub fn run(run_cmd: Option<String>, repo: Option<String>) -> Result<()> {
19    let is_repo = repo.is_some();
20    let _temp_dir = if let Some(repo_url) = repo {
21        let full_url = if repo_url.starts_with("http") || repo_url.contains('@')
22        {
23            repo_url
24        } else if let Some((provider, path)) = repo_url.split_once(':') {
25            match provider {
26                "gh" | "github" => format!("https://github.com/{path}.git"),
27                "gl" | "gitlab" => format!("https://gitlab.com/{path}.git"),
28                "cb" | "codeberg" => format!("https://codeberg.org/{path}.git"),
29                _ => return Err(anyhow!("Unknown provider: {provider}"))
30            }
31        } else {
32            format!("https://github.com/{repo_url}.git")
33        };
34
35        println!(
36            "{} Cloning repository: {}...",
37            "::".bold().blue(),
38            full_url.cyan()
39        );
40
41        let temp = tempfile::Builder::new().prefix("zoi-dev-").tempdir()?;
42        let status = Command::new("git")
43            .arg("clone")
44            .arg("--depth")
45            .arg("1")
46            .arg(full_url)
47            .arg(temp.path())
48            .status()?;
49
50        if !status.success() {
51            return Err(anyhow!("Failed to clone repository."));
52        }
53
54        std::env::set_current_dir(temp.path())?;
55        Some(temp)
56    } else {
57        None
58    };
59
60    let config = if is_repo {
61        project_config::load_with_env(&HashMap::new())?
62    } else {
63        project_config::load()?
64    };
65    println!(
66        "{} Entering development shell for project: {}",
67        "::".bold().blue(),
68        config.name.cyan().bold()
69    );
70
71    let (graph, _non_zoi_deps) = install::resolver::resolve_dependency_graph(
72        &config.pkgs,
73        Some(types::Scope::Project),
74        false,
75        true,
76        true,
77        None,
78        true,
79        Some(config.clone())
80    )?;
81
82    let mut missing_nodes = HashMap::new();
83    for (id, node) in &graph.nodes {
84        let request_source = crate::pkg::local::package_source_string(
85            &node.registry_handle,
86            &node.pkg.repo,
87            &node.pkg.name,
88            node.sub_package.as_deref(),
89            &node.version
90        );
91        if let Ok(request) = resolve::parse_source_string(&request_source) {
92            let matches = crate::pkg::local::find_installed_manifests_matching(
93                &request,
94                types::Scope::Project
95            )?;
96            if !matches
97                .iter()
98                .any(|manifest| manifest.version == node.version)
99            {
100                missing_nodes.insert(id.clone(), node.clone());
101            }
102        } else {
103            missing_nodes.insert(id.clone(), node.clone());
104        }
105    }
106
107    let install_plan =
108        install::plan::create_install_plan(&missing_nodes, None, false)?;
109    if !install_plan.is_empty() {
110        use std::sync::Mutex;
111
112        use rayon::prelude::*;
113
114        println!(
115            "{} Ensuring project dependencies are installed...",
116            "::".bold().blue()
117        );
118
119        let m_prep = MultiProgress::new();
120        let prepared_nodes = Mutex::new(HashMap::new());
121
122        missing_nodes.par_iter().try_for_each(
123            |(pkg_id, node)| -> Result<()> {
124                let action = install_plan.get(pkg_id).ok_or_else(|| {
125                    anyhow!("Install action not found for: {pkg_id}")
126                })?;
127
128                let prepared = install::installer::prepare_node(
129                    node,
130                    action,
131                    Some(&m_prep),
132                    None,
133                    false,
134                    false
135                )?;
136
137                let mut lock = prepared_nodes.lock().map_err(|e| {
138                    anyhow!(
139                        "Prepared nodes mutex poisoned during preparation: {e}"
140                    )
141                })?;
142                lock.insert(pkg_id.clone(), prepared);
143                Ok(())
144            }
145        )?;
146
147        let m = indicatif::MultiProgress::new();
148        let stages = graph.toposort()?;
149        for stage in stages {
150            stage.into_par_iter().try_for_each(|pkg_id| -> Result<()> {
151                let prepared = {
152                    let lock = prepared_nodes.lock().map_err(|e| {
153                        anyhow!(
154                            "Prepared nodes mutex poisoned during install: {e}"
155                        )
156                    })?;
157                    lock.get(&pkg_id).cloned()
158                };
159
160                if let Some(prepared) = prepared {
161                    let node = graph.nodes.get(&pkg_id).ok_or_else(|| {
162                        anyhow!("Package not found in graph: {pkg_id}")
163                    })?;
164
165                    install::installer::install_prepared_node(
166                        node,
167                        &prepared,
168                        Some(&m),
169                        true,
170                        true,
171                        true,
172                        false
173                    )?;
174                }
175                Ok(())
176            })?;
177        }
178    }
179
180    let mut env_vars: HashMap<String, String> = HashMap::new();
181
182    let mut bin_paths = Vec::new();
183    let mut lib_paths = Vec::new();
184    let mut include_paths = Vec::new();
185    let mut pkg_config_paths = Vec::new();
186
187    let sep = if cfg!(windows) { ";" } else { ":" };
188
189    for node in graph.nodes.values() {
190        let handle = &node.registry_handle;
191        let pkg = &node.pkg;
192        let package_dir =
193            local::get_package_dir(pkg.scope, handle, &pkg.repo, &pkg.name)?;
194        let version_dir = package_dir.join(&node.version);
195
196        let bin_dir = version_dir.join("bin");
197        if bin_dir.exists() {
198            bin_paths.push(bin_dir);
199        }
200
201        let lib_dir = version_dir.join("lib");
202        if lib_dir.exists() {
203            lib_paths.push(lib_dir.clone());
204            let pkgconfig_dir = lib_dir.join("pkgconfig");
205            if pkgconfig_dir.exists() {
206                pkg_config_paths.push(pkgconfig_dir);
207            }
208        }
209
210        let include_dir = version_dir.join("include");
211        if include_dir.exists() {
212            include_paths.push(include_dir);
213        }
214
215        let share_dir = version_dir.join("share");
216        if share_dir.exists() {
217            let pkgconfig_dir = share_dir.join("pkgconfig");
218            if pkgconfig_dir.exists() {
219                pkg_config_paths.push(pkgconfig_dir);
220            }
221        }
222    }
223
224    if !bin_paths.is_empty() {
225        let mut path = bin_paths
226            .iter()
227            .map(|p| p.to_string_lossy().to_string())
228            .collect::<Vec<_>>()
229            .join(sep);
230        if let Ok(old_path) = std::env::var("PATH") {
231            path = format!("{path}{sep}{old_path}");
232        }
233        env_vars.insert("PATH".to_string(), path);
234    }
235
236    if !lib_paths.is_empty() {
237        let lib_path_var = if cfg!(target_os = "macos") {
238            "DYLD_LIBRARY_PATH"
239        } else {
240            "LD_LIBRARY_PATH"
241        };
242        let mut path = lib_paths
243            .iter()
244            .map(|p| p.to_string_lossy().to_string())
245            .collect::<Vec<_>>()
246            .join(sep);
247        if let Ok(old_path) = std::env::var(lib_path_var) {
248            path = format!("{path}{sep}{old_path}");
249        }
250        env_vars.insert(lib_path_var.to_string(), path);
251    }
252
253    if !include_paths.is_empty() {
254        let path = include_paths
255            .iter()
256            .map(|p| p.to_string_lossy().to_string())
257            .collect::<Vec<_>>()
258            .join(sep);
259        for var in &["CPATH", "C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH"] {
260            let mut full_path = path.clone();
261            if let Ok(old_path) = std::env::var(var) {
262                full_path = format!("{full_path}{sep}{old_path}");
263            }
264            env_vars.insert(var.to_string(), full_path);
265        }
266    }
267
268    if !pkg_config_paths.is_empty() {
269        let mut path = pkg_config_paths
270            .iter()
271            .map(|p| p.to_string_lossy().to_string())
272            .collect::<Vec<_>>()
273            .join(sep);
274        if let Ok(old_path) = std::env::var("PKG_CONFIG_PATH") {
275            path = format!("{path}{sep}{old_path}");
276        }
277        env_vars.insert("PKG_CONFIG_PATH".to_string(), path);
278    }
279
280    if let Some(shell_spec) = &config.shell {
281        let platform = crate::pkg::utils::get_platform()?;
282        let extra_env = match &shell_spec.env {
283            project_config::PlatformOrEnvMap::EnvMap(m) => m.clone(),
284            project_config::PlatformOrEnvMap::Platform(p) => p
285                .get(&platform)
286                .or_else(|| p.get("default"))
287                .cloned()
288                .unwrap_or_default()
289        };
290        for (k, v) in extra_env {
291            env_vars.insert(k, v);
292        }
293    }
294
295    if let Some(cmd_str) = run_cmd {
296        println!("{} Running: {}", "::".bold().blue(), cmd_str.cyan());
297        let mut child = if cfg!(windows) {
298            Command::new("pwsh")
299                .arg("-Command")
300                .arg(&cmd_str)
301                .envs(&env_vars)
302                .spawn()?
303        } else {
304            Command::new("bash")
305                .arg("-c")
306                .arg(&cmd_str)
307                .envs(&env_vars)
308                .spawn()?
309        };
310        let status = child.wait()?;
311        if !status.success() {
312            std::process::exit(status.code().unwrap_or(1));
313        }
314    } else {
315        let shell_bin = std::env::var("SHELL").unwrap_or_else(|_| {
316            if cfg!(windows) {
317                "pwsh".to_string()
318            } else {
319                "bash".to_string()
320            }
321        });
322
323        println!(
324            "{} Entering dev shell (type 'exit' to leave)...",
325            "::".bold().green()
326        );
327
328        let mut child = Command::new(&shell_bin)
329            .envs(&env_vars)
330            .env("ZOI_SHELL", "dev")
331            .spawn()?;
332
333        let _ = child.wait()?;
334        println!("{} Exited dev shell.", "::".bold().blue());
335    }
336
337    Ok(())
338}