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