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}