1use 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
13pub 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 )?;
135
136 let mut lock = prepared_nodes.lock().map_err(|e| {
137 anyhow!(
138 "Prepared nodes mutex poisoned during preparation: {e}"
139 )
140 })?;
141 lock.insert(pkg_id.clone(), prepared);
142 Ok(())
143 }
144 )?;
145
146 let m = indicatif::MultiProgress::new();
147 let stages = graph.toposort()?;
148 for stage in stages {
149 stage.into_par_iter().try_for_each(|pkg_id| -> Result<()> {
150 let prepared = {
151 let lock = prepared_nodes.lock().map_err(|e| {
152 anyhow!(
153 "Prepared nodes mutex poisoned during install: {e}"
154 )
155 })?;
156 lock.get(&pkg_id).cloned()
157 };
158
159 if let Some(prepared) = prepared {
160 let node = graph.nodes.get(&pkg_id).ok_or_else(|| {
161 anyhow!("Package not found in graph: {pkg_id}")
162 })?;
163
164 install::installer::install_prepared_node(
165 node,
166 &prepared,
167 Some(&m),
168 true,
169 true,
170 true,
171 false
172 )?;
173 }
174 Ok(())
175 })?;
176 }
177 }
178
179 let mut env_vars: HashMap<String, String> = HashMap::new();
180
181 let mut bin_paths = Vec::new();
182 let mut lib_paths = Vec::new();
183 let mut include_paths = Vec::new();
184 let mut pkg_config_paths = Vec::new();
185
186 let sep = if cfg!(windows) { ";" } else { ":" };
187
188 for node in graph.nodes.values() {
189 let handle = &node.registry_handle;
190 let pkg = &node.pkg;
191 let package_dir =
192 local::get_package_dir(pkg.scope, handle, &pkg.repo, &pkg.name)?;
193 let version_dir = package_dir.join(&node.version);
194
195 let bin_dir = version_dir.join("bin");
196 if bin_dir.exists() {
197 bin_paths.push(bin_dir);
198 }
199
200 let lib_dir = version_dir.join("lib");
201 if lib_dir.exists() {
202 lib_paths.push(lib_dir.clone());
203 let pkgconfig_dir = lib_dir.join("pkgconfig");
204 if pkgconfig_dir.exists() {
205 pkg_config_paths.push(pkgconfig_dir);
206 }
207 }
208
209 let include_dir = version_dir.join("include");
210 if include_dir.exists() {
211 include_paths.push(include_dir);
212 }
213
214 let share_dir = version_dir.join("share");
215 if share_dir.exists() {
216 let pkgconfig_dir = share_dir.join("pkgconfig");
217 if pkgconfig_dir.exists() {
218 pkg_config_paths.push(pkgconfig_dir);
219 }
220 }
221 }
222
223 if !bin_paths.is_empty() {
224 let mut path = bin_paths
225 .iter()
226 .map(|p| p.to_string_lossy().to_string())
227 .collect::<Vec<_>>()
228 .join(sep);
229 if let Ok(old_path) = std::env::var("PATH") {
230 path = format!("{path}{sep}{old_path}");
231 }
232 env_vars.insert("PATH".to_string(), path);
233 }
234
235 if !lib_paths.is_empty() {
236 let lib_path_var = if cfg!(target_os = "macos") {
237 "DYLD_LIBRARY_PATH"
238 } else {
239 "LD_LIBRARY_PATH"
240 };
241 let mut path = lib_paths
242 .iter()
243 .map(|p| p.to_string_lossy().to_string())
244 .collect::<Vec<_>>()
245 .join(sep);
246 if let Ok(old_path) = std::env::var(lib_path_var) {
247 path = format!("{path}{sep}{old_path}");
248 }
249 env_vars.insert(lib_path_var.to_string(), path);
250 }
251
252 if !include_paths.is_empty() {
253 let path = include_paths
254 .iter()
255 .map(|p| p.to_string_lossy().to_string())
256 .collect::<Vec<_>>()
257 .join(sep);
258 for var in &["CPATH", "C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH"] {
259 let mut full_path = path.clone();
260 if let Ok(old_path) = std::env::var(var) {
261 full_path = format!("{full_path}{sep}{old_path}");
262 }
263 env_vars.insert(var.to_string(), full_path);
264 }
265 }
266
267 if !pkg_config_paths.is_empty() {
268 let mut path = pkg_config_paths
269 .iter()
270 .map(|p| p.to_string_lossy().to_string())
271 .collect::<Vec<_>>()
272 .join(sep);
273 if let Ok(old_path) = std::env::var("PKG_CONFIG_PATH") {
274 path = format!("{path}{sep}{old_path}");
275 }
276 env_vars.insert("PKG_CONFIG_PATH".to_string(), path);
277 }
278
279 if let Some(shell_spec) = &config.shell {
280 let platform = crate::pkg::utils::get_platform()?;
281 let extra_env = match &shell_spec.env {
282 project_config::PlatformOrEnvMap::EnvMap(m) => m.clone(),
283 project_config::PlatformOrEnvMap::Platform(p) => p
284 .get(&platform)
285 .or_else(|| p.get("default"))
286 .cloned()
287 .unwrap_or_default()
288 };
289 for (k, v) in extra_env {
290 env_vars.insert(k, v);
291 }
292 }
293
294 if let Some(cmd_str) = run_cmd {
295 println!("{} Running: {}", "::".bold().blue(), cmd_str.cyan());
296 let mut child = if cfg!(windows) {
297 Command::new("pwsh")
298 .arg("-Command")
299 .arg(&cmd_str)
300 .envs(&env_vars)
301 .spawn()?
302 } else {
303 Command::new("bash")
304 .arg("-c")
305 .arg(&cmd_str)
306 .envs(&env_vars)
307 .spawn()?
308 };
309 let status = child.wait()?;
310 if !status.success() {
311 std::process::exit(status.code().unwrap_or(1));
312 }
313 } else {
314 let shell_bin = std::env::var("SHELL").unwrap_or_else(|_| {
315 if cfg!(windows) {
316 "pwsh".to_string()
317 } else {
318 "bash".to_string()
319 }
320 });
321
322 println!(
323 "{} Entering dev shell (type 'exit' to leave)...",
324 "::".bold().green()
325 );
326
327 let mut child = Command::new(&shell_bin)
328 .envs(&env_vars)
329 .env("ZOI_SHELL", "dev")
330 .spawn()?;
331
332 let _ = child.wait()?;
333 println!("{} Exited dev shell.", "::".bold().blue());
334 }
335
336 Ok(())
337}