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}