use crate::pkg::{install, local, resolve, types};
use anyhow::{Result, anyhow};
use colored::*;
use indicatif::MultiProgress;
use std::collections::HashMap;
use std::process::Command;
use zoi_project::config as project_config;
pub fn run(run_cmd: Option<String>, repo: Option<String>) -> Result<()> {
let is_repo = repo.is_some();
let _temp_dir = if let Some(repo_url) = repo {
let full_url = if repo_url.starts_with("http") || repo_url.contains('@') {
repo_url
} else if let Some((provider, path)) = repo_url.split_once(':') {
match provider {
"gh" | "github" => format!("https://github.com/{}.git", path),
"gl" | "gitlab" => format!("https://gitlab.com/{}.git", path),
"cb" | "codeberg" => format!("https://codeberg.org/{}.git", path),
_ => return Err(anyhow!("Unknown provider: {}", provider)),
}
} else {
format!("https://github.com/{}.git", repo_url)
};
println!(
"{} Cloning repository: {}...",
"::".bold().blue(),
full_url.cyan()
);
let temp = tempfile::Builder::new().prefix("zoi-dev-").tempdir()?;
let status = Command::new("git")
.arg("clone")
.arg("--depth")
.arg("1")
.arg(full_url)
.arg(temp.path())
.status()?;
if !status.success() {
return Err(anyhow!("Failed to clone repository."));
}
std::env::set_current_dir(temp.path())?;
Some(temp)
} else {
None
};
let config = if is_repo {
project_config::load_with_env(HashMap::new())?
} else {
project_config::load()?
};
println!(
"{} Entering development shell for project: {}",
"::".bold().blue(),
config.name.cyan().bold()
);
let (graph, _non_zoi_deps) = install::resolver::resolve_dependency_graph(
&config.pkgs,
Some(types::Scope::Project),
false,
true,
true,
None,
true,
Some(config.clone()),
)?;
let mut missing_nodes = HashMap::new();
for (id, node) in &graph.nodes {
let request_source = crate::pkg::local::package_source_string(
&node.registry_handle,
&node.pkg.repo,
&node.pkg.name,
node.sub_package.as_deref(),
&node.version,
);
if let Ok(request) = resolve::parse_source_string(&request_source) {
let matches = crate::pkg::local::find_installed_manifests_matching(
&request,
types::Scope::Project,
)?;
if !matches
.iter()
.any(|manifest| manifest.version == node.version)
{
missing_nodes.insert(id.clone(), node.clone());
}
} else {
missing_nodes.insert(id.clone(), node.clone());
}
}
let install_plan = install::plan::create_install_plan(&missing_nodes, None, false)?;
if !install_plan.is_empty() {
println!(
"{} Ensuring project dependencies are installed...",
"::".bold().blue()
);
use rayon::prelude::*;
use std::sync::Mutex;
let m_prep = MultiProgress::new();
let prepared_nodes = Mutex::new(HashMap::new());
missing_nodes
.par_iter()
.try_for_each(|(pkg_id, node)| -> Result<()> {
let action = install_plan
.get(pkg_id)
.ok_or_else(|| anyhow!("Install action not found for: {}", pkg_id))?;
let prepared =
install::installer::prepare_node(node, action, Some(&m_prep), None, false)?;
let mut lock = prepared_nodes.lock().map_err(|e| {
anyhow!("Prepared nodes mutex poisoned during preparation: {}", e)
})?;
lock.insert(pkg_id.clone(), prepared);
Ok(())
})?;
let m = indicatif::MultiProgress::new();
let stages = graph.toposort()?;
for stage in stages {
stage.into_par_iter().try_for_each(|pkg_id| -> Result<()> {
let prepared = {
let lock = prepared_nodes.lock().map_err(|e| {
anyhow!("Prepared nodes mutex poisoned during install: {}", e)
})?;
lock.get(&pkg_id).cloned()
};
if let Some(prepared) = prepared {
let node = graph
.nodes
.get(&pkg_id)
.ok_or_else(|| anyhow!("Package not found in graph: {}", pkg_id))?;
install::installer::install_prepared_node(
node,
&prepared,
Some(&m),
true,
true,
true,
false,
)?;
}
Ok(())
})?;
}
}
let mut env_vars: HashMap<String, String> = HashMap::new();
let mut bin_paths = Vec::new();
let mut lib_paths = Vec::new();
let mut include_paths = Vec::new();
let mut pkg_config_paths = Vec::new();
let sep = if cfg!(windows) { ";" } else { ":" };
for node in graph.nodes.values() {
let handle = &node.registry_handle;
let pkg = &node.pkg;
let package_dir = local::get_package_dir(pkg.scope, handle, &pkg.repo, &pkg.name)?;
let version_dir = package_dir.join(&node.version);
let bin_dir = version_dir.join("bin");
if bin_dir.exists() {
bin_paths.push(bin_dir);
}
let lib_dir = version_dir.join("lib");
if lib_dir.exists() {
lib_paths.push(lib_dir.clone());
let pkgconfig_dir = lib_dir.join("pkgconfig");
if pkgconfig_dir.exists() {
pkg_config_paths.push(pkgconfig_dir);
}
}
let include_dir = version_dir.join("include");
if include_dir.exists() {
include_paths.push(include_dir);
}
let share_dir = version_dir.join("share");
if share_dir.exists() {
let pkgconfig_dir = share_dir.join("pkgconfig");
if pkgconfig_dir.exists() {
pkg_config_paths.push(pkgconfig_dir);
}
}
}
if !bin_paths.is_empty() {
let mut path = bin_paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(sep);
if let Ok(old_path) = std::env::var("PATH") {
path = format!("{}{}{}", path, sep, old_path);
}
env_vars.insert("PATH".to_string(), path);
}
if !lib_paths.is_empty() {
let lib_path_var = if cfg!(target_os = "macos") {
"DYLD_LIBRARY_PATH"
} else {
"LD_LIBRARY_PATH"
};
let mut path = lib_paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(sep);
if let Ok(old_path) = std::env::var(lib_path_var) {
path = format!("{}{}{}", path, sep, old_path);
}
env_vars.insert(lib_path_var.to_string(), path);
}
if !include_paths.is_empty() {
let path = include_paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(sep);
for var in &["CPATH", "C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH"] {
let mut full_path = path.clone();
if let Ok(old_path) = std::env::var(var) {
full_path = format!("{}{}{}", full_path, sep, old_path);
}
env_vars.insert(var.to_string(), full_path);
}
}
if !pkg_config_paths.is_empty() {
let mut path = pkg_config_paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(sep);
if let Ok(old_path) = std::env::var("PKG_CONFIG_PATH") {
path = format!("{}{}{}", path, sep, old_path);
}
env_vars.insert("PKG_CONFIG_PATH".to_string(), path);
}
if let Some(shell_spec) = &config.shell {
let platform = crate::pkg::utils::get_platform()?;
let extra_env = match &shell_spec.env {
project_config::PlatformOrEnvMap::EnvMap(m) => m.clone(),
project_config::PlatformOrEnvMap::Platform(p) => p
.get(&platform)
.or_else(|| p.get("default"))
.cloned()
.unwrap_or_default(),
};
for (k, v) in extra_env {
env_vars.insert(k, v);
}
}
if let Some(cmd_str) = run_cmd {
println!("{} Running: {}", "::".bold().blue(), cmd_str.cyan());
let mut child = if cfg!(windows) {
Command::new("pwsh")
.arg("-Command")
.arg(&cmd_str)
.envs(&env_vars)
.spawn()?
} else {
Command::new("bash")
.arg("-c")
.arg(&cmd_str)
.envs(&env_vars)
.spawn()?
};
let status = child.wait()?;
if !status.success() {
std::process::exit(status.code().unwrap_or(1));
}
} else {
let shell_bin = std::env::var("SHELL").unwrap_or_else(|_| {
if cfg!(windows) {
"pwsh".to_string()
} else {
"bash".to_string()
}
});
println!(
"{} Entering dev shell (type 'exit' to leave)...",
"::".bold().green()
);
let mut child = Command::new(&shell_bin)
.envs(&env_vars)
.env("ZOI_SHELL", "dev")
.spawn()?;
let _ = child.wait()?;
println!("{} Exited dev shell.", "::".bold().blue());
}
Ok(())
}