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        Some(config.clone()),
70    )?;
71
72    let mut missing_nodes = HashMap::new();
73    for (id, node) in &graph.nodes {
74        let request_source = crate::pkg::local::package_source_string(
75            &node.registry_handle,
76            &node.pkg.repo,
77            &node.pkg.name,
78            node.sub_package.as_deref(),
79            &node.version,
80        );
81        if let Ok(request) = resolve::parse_source_string(&request_source) {
82            let matches = crate::pkg::local::find_installed_manifests_matching(
83                &request,
84                types::Scope::Project,
85            )?;
86            if !matches
87                .iter()
88                .any(|manifest| manifest.version == node.version)
89            {
90                missing_nodes.insert(id.clone(), node.clone());
91            }
92        } else {
93            missing_nodes.insert(id.clone(), node.clone());
94        }
95    }
96
97    let install_plan = install::plan::create_install_plan(&missing_nodes, None, false)?;
98    if !install_plan.is_empty() {
99        println!(
100            "{} Ensuring project dependencies are installed...",
101            "::".bold().blue()
102        );
103
104        use rayon::prelude::*;
105        use std::sync::Mutex;
106
107        let m_prep = MultiProgress::new();
108        let prepared_nodes = Mutex::new(HashMap::new());
109
110        missing_nodes
111            .par_iter()
112            .try_for_each(|(pkg_id, node)| -> Result<()> {
113                let action = install_plan
114                    .get(pkg_id)
115                    .ok_or_else(|| anyhow!("Install action not found for: {}", pkg_id))?;
116
117                let prepared =
118                    install::installer::prepare_node(node, action, Some(&m_prep), None, false)?;
119
120                let mut lock = prepared_nodes.lock().map_err(|e| {
121                    anyhow!("Prepared nodes mutex poisoned during preparation: {}", e)
122                })?;
123                lock.insert(pkg_id.clone(), prepared);
124                Ok(())
125            })?;
126
127        let m = indicatif::MultiProgress::new();
128        let stages = graph.toposort()?;
129        for stage in stages {
130            stage.into_par_iter().try_for_each(|pkg_id| -> Result<()> {
131                let prepared = {
132                    let lock = prepared_nodes.lock().map_err(|e| {
133                        anyhow!("Prepared nodes mutex poisoned during install: {}", e)
134                    })?;
135                    lock.get(&pkg_id).cloned()
136                };
137
138                if let Some(prepared) = prepared {
139                    let node = graph
140                        .nodes
141                        .get(&pkg_id)
142                        .ok_or_else(|| anyhow!("Package not found in graph: {}", pkg_id))?;
143
144                    install::installer::install_prepared_node(
145                        node,
146                        &prepared,
147                        Some(&m),
148                        true,
149                        true,
150                        true,
151                        false,
152                    )?;
153                }
154                Ok(())
155            })?;
156        }
157    }
158
159    let mut env_vars: HashMap<String, String> = HashMap::new();
160
161    let mut bin_paths = Vec::new();
162    let mut lib_paths = Vec::new();
163    let mut include_paths = Vec::new();
164    let mut pkg_config_paths = Vec::new();
165
166    let sep = if cfg!(windows) { ";" } else { ":" };
167
168    for node in graph.nodes.values() {
169        let handle = &node.registry_handle;
170        let pkg = &node.pkg;
171        let package_dir = local::get_package_dir(pkg.scope, handle, &pkg.repo, &pkg.name)?;
172        let version_dir = package_dir.join(&node.version);
173
174        let bin_dir = version_dir.join("bin");
175        if bin_dir.exists() {
176            bin_paths.push(bin_dir);
177        }
178
179        let lib_dir = version_dir.join("lib");
180        if lib_dir.exists() {
181            lib_paths.push(lib_dir.clone());
182            let pkgconfig_dir = lib_dir.join("pkgconfig");
183            if pkgconfig_dir.exists() {
184                pkg_config_paths.push(pkgconfig_dir);
185            }
186        }
187
188        let include_dir = version_dir.join("include");
189        if include_dir.exists() {
190            include_paths.push(include_dir);
191        }
192
193        let share_dir = version_dir.join("share");
194        if share_dir.exists() {
195            let pkgconfig_dir = share_dir.join("pkgconfig");
196            if pkgconfig_dir.exists() {
197                pkg_config_paths.push(pkgconfig_dir);
198            }
199        }
200    }
201
202    if !bin_paths.is_empty() {
203        let mut path = bin_paths
204            .iter()
205            .map(|p| p.to_string_lossy().to_string())
206            .collect::<Vec<_>>()
207            .join(sep);
208        if let Ok(old_path) = std::env::var("PATH") {
209            path = format!("{}{}{}", path, sep, old_path);
210        }
211        env_vars.insert("PATH".to_string(), path);
212    }
213
214    if !lib_paths.is_empty() {
215        let lib_path_var = if cfg!(target_os = "macos") {
216            "DYLD_LIBRARY_PATH"
217        } else {
218            "LD_LIBRARY_PATH"
219        };
220        let mut path = lib_paths
221            .iter()
222            .map(|p| p.to_string_lossy().to_string())
223            .collect::<Vec<_>>()
224            .join(sep);
225        if let Ok(old_path) = std::env::var(lib_path_var) {
226            path = format!("{}{}{}", path, sep, old_path);
227        }
228        env_vars.insert(lib_path_var.to_string(), path);
229    }
230
231    if !include_paths.is_empty() {
232        let path = include_paths
233            .iter()
234            .map(|p| p.to_string_lossy().to_string())
235            .collect::<Vec<_>>()
236            .join(sep);
237        for var in &["CPATH", "C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH"] {
238            let mut full_path = path.clone();
239            if let Ok(old_path) = std::env::var(var) {
240                full_path = format!("{}{}{}", full_path, sep, old_path);
241            }
242            env_vars.insert(var.to_string(), full_path);
243        }
244    }
245
246    if !pkg_config_paths.is_empty() {
247        let mut path = pkg_config_paths
248            .iter()
249            .map(|p| p.to_string_lossy().to_string())
250            .collect::<Vec<_>>()
251            .join(sep);
252        if let Ok(old_path) = std::env::var("PKG_CONFIG_PATH") {
253            path = format!("{}{}{}", path, sep, old_path);
254        }
255        env_vars.insert("PKG_CONFIG_PATH".to_string(), path);
256    }
257
258    if let Some(shell_spec) = &config.shell {
259        let platform = crate::pkg::utils::get_platform()?;
260        let extra_env = match &shell_spec.env {
261            project_config::PlatformOrEnvMap::EnvMap(m) => m.clone(),
262            project_config::PlatformOrEnvMap::Platform(p) => p
263                .get(&platform)
264                .or_else(|| p.get("default"))
265                .cloned()
266                .unwrap_or_default(),
267        };
268        for (k, v) in extra_env {
269            env_vars.insert(k, v);
270        }
271    }
272
273    if let Some(cmd_str) = run_cmd {
274        println!("{} Running: {}", "::".bold().blue(), cmd_str.cyan());
275        let mut child = if cfg!(windows) {
276            Command::new("pwsh")
277                .arg("-Command")
278                .arg(&cmd_str)
279                .envs(&env_vars)
280                .spawn()?
281        } else {
282            Command::new("bash")
283                .arg("-c")
284                .arg(&cmd_str)
285                .envs(&env_vars)
286                .spawn()?
287        };
288        let status = child.wait()?;
289        if !status.success() {
290            std::process::exit(status.code().unwrap_or(1));
291        }
292    } else {
293        let shell_bin = std::env::var("SHELL").unwrap_or_else(|_| {
294            if cfg!(windows) {
295                "pwsh".to_string()
296            } else {
297                "bash".to_string()
298            }
299        });
300
301        println!(
302            "{} Entering dev shell (type 'exit' to leave)...",
303            "::".bold().green()
304        );
305
306        let mut child = Command::new(&shell_bin)
307            .envs(&env_vars)
308            .env("ZOI_SHELL", "dev")
309            .spawn()?;
310
311        let _ = child.wait()?;
312        println!("{} Exited dev shell.", "::".bold().blue());
313    }
314
315    Ok(())
316}