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 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}