Skip to main content

zoi_cli/cmd/
dev.rs

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