diff --git a/src/main.rs b/src/main.rs index b09e4b3..c20e476 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod interpreter; mod lexer; mod lsp; mod modules; +mod package; mod parser; mod schema; mod typechecker; @@ -133,6 +134,10 @@ fn main() { std::process::exit(1); } } + "pkg" => { + // Package manager + handle_pkg_command(&args[2..]); + } path => { // Run a file run_file(path); @@ -156,6 +161,7 @@ fn print_help() { println!(" lux watch Watch and re-run on changes"); println!(" lux debug Start interactive debugger"); println!(" lux init [name] Initialize a new project"); + println!(" lux pkg Package manager (install, add, remove, list, update)"); println!(" lux --lsp Start LSP server (for IDE integration)"); println!(" lux --help Show this help"); println!(" lux --version Show version"); @@ -382,6 +388,156 @@ fn watch_file(path: &str) { } } +fn handle_pkg_command(args: &[String]) { + use package::{PackageManager, DependencySource}; + use std::path::PathBuf; + + if args.is_empty() { + print_pkg_help(); + return; + } + + // Help doesn't require being in a project + if matches!(args[0].as_str(), "help" | "--help" | "-h") { + print_pkg_help(); + return; + } + + let project_root = match PackageManager::find_project_root() { + Some(root) => root, + None => { + eprintln!("Error: Not in a Lux project (no lux.toml found)"); + eprintln!("Run 'lux init' to create a new project"); + std::process::exit(1); + } + }; + + let pkg = PackageManager::new(&project_root); + + match args[0].as_str() { + "install" | "i" => { + if let Err(e) = pkg.install() { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + "add" => { + if args.len() < 2 { + eprintln!("Usage: lux pkg add [version] [--git ] [--path ]"); + std::process::exit(1); + } + let name = &args[1]; + let mut version = "0.0.0".to_string(); + let mut source = DependencySource::Registry; + + let mut i = 2; + while i < args.len() { + match args[i].as_str() { + "--git" => { + if i + 1 < args.len() { + let url = args[i + 1].clone(); + let branch = if i + 3 < args.len() && args[i + 2] == "--branch" { + i += 2; + Some(args[i + 1].clone()) + } else { + None + }; + source = DependencySource::Git { url, branch }; + i += 2; + } else { + eprintln!("--git requires a URL"); + std::process::exit(1); + } + } + "--path" => { + if i + 1 < args.len() { + source = DependencySource::Path { + path: PathBuf::from(&args[i + 1]), + }; + i += 2; + } else { + eprintln!("--path requires a path"); + std::process::exit(1); + } + } + v if !v.starts_with('-') => { + version = v.to_string(); + i += 1; + } + _ => i += 1, + } + } + + if let Err(e) = pkg.add(name, &version, source) { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + "remove" | "rm" => { + if args.len() < 2 { + eprintln!("Usage: lux pkg remove "); + std::process::exit(1); + } + if let Err(e) = pkg.remove(&args[1]) { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + "list" | "ls" => { + if let Err(e) = pkg.list() { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + "update" => { + if let Err(e) = pkg.update() { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + "clean" => { + if let Err(e) = pkg.clean() { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + "help" | "--help" | "-h" => { + print_pkg_help(); + } + unknown => { + eprintln!("Unknown package command: {}", unknown); + print_pkg_help(); + std::process::exit(1); + } + } +} + +fn print_pkg_help() { + println!("Lux Package Manager"); + println!(); + println!("Usage: lux pkg [options]"); + println!(); + println!("Commands:"); + println!(" install, i Install all dependencies from lux.toml"); + println!(" add Add a dependency"); + println!(" Options:"); + println!(" [version] Specify version (default: 0.0.0)"); + println!(" --git Install from git repository"); + println!(" --branch Git branch (with --git)"); + println!(" --path Install from local path"); + println!(" remove, rm Remove a dependency"); + println!(" list, ls List dependencies and their status"); + println!(" update Update all dependencies"); + println!(" clean Remove installed packages"); + println!(); + println!("Examples:"); + println!(" lux pkg install"); + println!(" lux pkg add http 1.0.0"); + println!(" lux pkg add mylib --git https://github.com/user/mylib"); + println!(" lux pkg add local-lib --path ../lib"); + println!(" lux pkg remove http"); +} + fn init_project(name: Option<&str>) { use std::fs; use std::path::Path; diff --git a/src/package.rs b/src/package.rs new file mode 100644 index 0000000..d8f320c --- /dev/null +++ b/src/package.rs @@ -0,0 +1,621 @@ +//! Package manager for Lux +//! +//! Handles dependency management, package installation, and version resolution. + +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::io::{self, Write}; + +/// Package manifest (lux.toml) +#[derive(Debug, Clone)] +pub struct Manifest { + pub project: ProjectInfo, + pub dependencies: HashMap, +} + +/// Project information from lux.toml +#[derive(Debug, Clone)] +pub struct ProjectInfo { + pub name: String, + pub version: String, + pub description: Option, + pub authors: Vec, + pub license: Option, +} + +/// A package dependency +#[derive(Debug, Clone)] +pub struct Dependency { + pub name: String, + pub version: String, + pub source: DependencySource, +} + +/// Source of a dependency +#[derive(Debug, Clone)] +pub enum DependencySource { + /// From the Lux package registry + Registry, + /// From a git repository + Git { url: String, branch: Option }, + /// From a local path + Path { path: PathBuf }, +} + +/// Package manager +pub struct PackageManager { + /// Root directory of the project + project_root: PathBuf, + /// Directory where packages are cached + cache_dir: PathBuf, + /// Directory where packages are installed (project-local) + packages_dir: PathBuf, +} + +impl PackageManager { + pub fn new(project_root: &Path) -> Self { + let cache_dir = dirs::cache_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("lux") + .join("packages"); + + let packages_dir = project_root.join(".lux_packages"); + + Self { + project_root: project_root.to_path_buf(), + cache_dir, + packages_dir, + } + } + + /// Find the project root by looking for lux.toml + pub fn find_project_root() -> Option { + let mut current = std::env::current_dir().ok()?; + + loop { + if current.join("lux.toml").exists() { + return Some(current); + } + + if !current.pop() { + return None; + } + } + } + + /// Load the manifest from lux.toml + pub fn load_manifest(&self) -> Result { + let manifest_path = self.project_root.join("lux.toml"); + + if !manifest_path.exists() { + return Err("No lux.toml found in project root".to_string()); + } + + let content = fs::read_to_string(&manifest_path) + .map_err(|e| format!("Failed to read lux.toml: {}", e))?; + + parse_manifest(&content) + } + + /// Save the manifest to lux.toml + pub fn save_manifest(&self, manifest: &Manifest) -> Result<(), String> { + let manifest_path = self.project_root.join("lux.toml"); + let content = format_manifest(manifest); + + fs::write(&manifest_path, content) + .map_err(|e| format!("Failed to write lux.toml: {}", e)) + } + + /// Add a dependency to the project + pub fn add(&self, name: &str, version: &str, source: DependencySource) -> Result<(), String> { + let mut manifest = self.load_manifest()?; + + let dep = Dependency { + name: name.to_string(), + version: version.to_string(), + source, + }; + + manifest.dependencies.insert(name.to_string(), dep); + self.save_manifest(&manifest)?; + + println!("Added {} v{} to dependencies", name, version); + Ok(()) + } + + /// Remove a dependency from the project + pub fn remove(&self, name: &str) -> Result<(), String> { + let mut manifest = self.load_manifest()?; + + if manifest.dependencies.remove(name).is_some() { + self.save_manifest(&manifest)?; + + // Remove from packages dir if it exists + let pkg_dir = self.packages_dir.join(name); + if pkg_dir.exists() { + fs::remove_dir_all(&pkg_dir) + .map_err(|e| format!("Failed to remove package directory: {}", e))?; + } + + println!("Removed {} from dependencies", name); + Ok(()) + } else { + Err(format!("Dependency '{}' not found", name)) + } + } + + /// Install all dependencies + pub fn install(&self) -> Result<(), String> { + let manifest = self.load_manifest()?; + + if manifest.dependencies.is_empty() { + println!("No dependencies to install."); + return Ok(()); + } + + // Create packages directory + fs::create_dir_all(&self.packages_dir) + .map_err(|e| format!("Failed to create packages directory: {}", e))?; + + println!("Installing {} dependencies...", manifest.dependencies.len()); + println!(); + + for (name, dep) in &manifest.dependencies { + self.install_dependency(dep)?; + } + + println!(); + println!("Done! Installed {} packages.", manifest.dependencies.len()); + Ok(()) + } + + /// Install a single dependency + fn install_dependency(&self, dep: &Dependency) -> Result<(), String> { + print!(" Installing {} v{}... ", dep.name, dep.version); + io::stdout().flush().unwrap(); + + let dest_dir = self.packages_dir.join(&dep.name); + + match &dep.source { + DependencySource::Registry => { + // For now, simulate registry installation + // In a real implementation, this would fetch from a package registry + self.install_from_registry(dep, &dest_dir)?; + } + DependencySource::Git { url, branch } => { + self.install_from_git(url, branch.as_deref(), &dest_dir)?; + } + DependencySource::Path { path } => { + self.install_from_path(path, &dest_dir)?; + } + } + + println!("done"); + Ok(()) + } + + fn install_from_registry(&self, dep: &Dependency, dest: &Path) -> Result<(), String> { + // Check if already installed with correct version + let version_file = dest.join(".version"); + if version_file.exists() { + let installed_version = fs::read_to_string(&version_file).unwrap_or_default(); + if installed_version.trim() == dep.version { + return Ok(()); + } + } + + // Check cache first + let cache_path = self.cache_dir.join(&dep.name).join(&dep.version); + + if cache_path.exists() { + // Copy from cache + copy_dir_recursive(&cache_path, dest)?; + } else { + // Create placeholder package (in real impl, would download) + fs::create_dir_all(dest) + .map_err(|e| format!("Failed to create package directory: {}", e))?; + + // Create a lib.lux placeholder + let lib_content = format!( + "// Package: {} v{}\n// This is a placeholder - real package would be downloaded from registry\n\n", + dep.name, dep.version + ); + fs::write(dest.join("lib.lux"), lib_content) + .map_err(|e| format!("Failed to create lib.lux: {}", e))?; + } + + // Write version file + fs::write(&version_file, &dep.version) + .map_err(|e| format!("Failed to write version file: {}", e))?; + + Ok(()) + } + + fn install_from_git(&self, url: &str, branch: Option<&str>, dest: &Path) -> Result<(), String> { + // Remove existing if present + if dest.exists() { + fs::remove_dir_all(dest) + .map_err(|e| format!("Failed to remove existing directory: {}", e))?; + } + + // Clone the repository + let mut cmd = std::process::Command::new("git"); + cmd.arg("clone") + .arg("--depth").arg("1"); + + if let Some(b) = branch { + cmd.arg("--branch").arg(b); + } + + cmd.arg(url).arg(dest); + + let output = cmd.output() + .map_err(|e| format!("Failed to run git: {}", e))?; + + if !output.status.success() { + return Err(format!( + "Git clone failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + + // Remove .git directory to save space + let git_dir = dest.join(".git"); + if git_dir.exists() { + fs::remove_dir_all(&git_dir).ok(); + } + + Ok(()) + } + + fn install_from_path(&self, source: &Path, dest: &Path) -> Result<(), String> { + let source = if source.is_absolute() { + source.to_path_buf() + } else { + self.project_root.join(source) + }; + + if !source.exists() { + return Err(format!("Source path does not exist: {}", source.display())); + } + + // Remove existing if present + if dest.exists() { + fs::remove_dir_all(dest) + .map_err(|e| format!("Failed to remove existing directory: {}", e))?; + } + + // Copy the directory + copy_dir_recursive(&source, dest)?; + + Ok(()) + } + + /// List installed packages + pub fn list(&self) -> Result<(), String> { + let manifest = self.load_manifest()?; + + if manifest.dependencies.is_empty() { + println!("No dependencies in lux.toml"); + return Ok(()); + } + + println!("Dependencies:"); + for (name, dep) in &manifest.dependencies { + let source_info = match &dep.source { + DependencySource::Registry => "registry".to_string(), + DependencySource::Git { url, branch } => { + if let Some(b) = branch { + format!("git: {} ({})", url, b) + } else { + format!("git: {}", url) + } + } + DependencySource::Path { path } => format!("path: {}", path.display()), + }; + + let installed = self.packages_dir.join(name).exists(); + let status = if installed { "✓" } else { "✗" }; + + println!(" {} {} v{} [{}]", status, name, dep.version, source_info); + } + + Ok(()) + } + + /// Update all dependencies + pub fn update(&self) -> Result<(), String> { + let manifest = self.load_manifest()?; + + if manifest.dependencies.is_empty() { + println!("No dependencies to update."); + return Ok(()); + } + + println!("Updating {} dependencies...", manifest.dependencies.len()); + println!(); + + for (name, dep) in &manifest.dependencies { + // Force reinstall + let dest_dir = self.packages_dir.join(name); + if dest_dir.exists() { + fs::remove_dir_all(&dest_dir).ok(); + } + self.install_dependency(dep)?; + } + + println!(); + println!("Done!"); + Ok(()) + } + + /// Get the search paths for installed packages + pub fn get_package_paths(&self) -> Vec { + let mut paths = vec![]; + + if self.packages_dir.exists() { + paths.push(self.packages_dir.clone()); + } + + // Also include global cache + if self.cache_dir.exists() { + paths.push(self.cache_dir.clone()); + } + + paths + } + + /// Clean the packages directory + pub fn clean(&self) -> Result<(), String> { + if self.packages_dir.exists() { + fs::remove_dir_all(&self.packages_dir) + .map_err(|e| format!("Failed to remove packages directory: {}", e))?; + println!("Cleaned .lux_packages directory"); + } else { + println!("Nothing to clean"); + } + Ok(()) + } +} + +/// Parse a lux.toml manifest +fn parse_manifest(content: &str) -> Result { + let mut project = ProjectInfo { + name: String::new(), + version: String::new(), + description: None, + authors: vec![], + license: None, + }; + let mut dependencies = HashMap::new(); + + let mut current_section = ""; + + for line in content.lines() { + let line = line.trim(); + + // Skip comments and empty lines + if line.is_empty() || line.starts_with('#') { + continue; + } + + // Section header + if line.starts_with('[') && line.ends_with(']') { + current_section = &line[1..line.len()-1]; + continue; + } + + // Key = value + if let Some(eq_pos) = line.find('=') { + let key = line[..eq_pos].trim(); + let value = line[eq_pos+1..].trim(); + let value = value.trim_matches('"'); + + match current_section { + "project" => { + match key { + "name" => project.name = value.to_string(), + "version" => project.version = value.to_string(), + "description" => project.description = Some(value.to_string()), + "license" => project.license = Some(value.to_string()), + "authors" => { + // Simple array parsing + let authors_str = value.trim_matches(|c| c == '[' || c == ']'); + project.authors = authors_str + .split(',') + .map(|s| s.trim().trim_matches('"').to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } + _ => {} + } + } + "dependencies" => { + // Parse dependency + let dep = parse_dependency(key, value)?; + dependencies.insert(key.to_string(), dep); + } + _ => {} + } + } + } + + if project.name.is_empty() { + return Err("Missing project name in lux.toml".to_string()); + } + if project.version.is_empty() { + return Err("Missing project version in lux.toml".to_string()); + } + + Ok(Manifest { project, dependencies }) +} + +fn parse_dependency(name: &str, value: &str) -> Result { + let value = value.trim(); + + // Simple version string: "1.0.0" + if value.starts_with('"') || value.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) { + let version = value.trim_matches('"'); + return Ok(Dependency { + name: name.to_string(), + version: version.to_string(), + source: DependencySource::Registry, + }); + } + + // Inline table: { version = "1.0", git = "..." } + if value.starts_with('{') && value.ends_with('}') { + let inner = &value[1..value.len()-1]; + let mut version = "0.0.0".to_string(); + let mut git_url = None; + let mut git_branch = None; + let mut path = None; + + for part in inner.split(',') { + let part = part.trim(); + if let Some(eq_pos) = part.find('=') { + let k = part[..eq_pos].trim(); + let v = part[eq_pos+1..].trim().trim_matches('"'); + + match k { + "version" => version = v.to_string(), + "git" => git_url = Some(v.to_string()), + "branch" => git_branch = Some(v.to_string()), + "path" => path = Some(PathBuf::from(v)), + _ => {} + } + } + } + + let source = if let Some(url) = git_url { + DependencySource::Git { url, branch: git_branch } + } else if let Some(p) = path { + DependencySource::Path { path: p } + } else { + DependencySource::Registry + }; + + return Ok(Dependency { + name: name.to_string(), + version, + source, + }); + } + + Err(format!("Invalid dependency format for {}: {}", name, value)) +} + +/// Format a manifest as TOML +fn format_manifest(manifest: &Manifest) -> String { + let mut output = String::new(); + + output.push_str("[project]\n"); + output.push_str(&format!("name = \"{}\"\n", manifest.project.name)); + output.push_str(&format!("version = \"{}\"\n", manifest.project.version)); + + if let Some(desc) = &manifest.project.description { + output.push_str(&format!("description = \"{}\"\n", desc)); + } + + if !manifest.project.authors.is_empty() { + let authors: Vec = manifest.project.authors.iter() + .map(|a| format!("\"{}\"", a)) + .collect(); + output.push_str(&format!("authors = [{}]\n", authors.join(", "))); + } + + if let Some(license) = &manifest.project.license { + output.push_str(&format!("license = \"{}\"\n", license)); + } + + output.push_str("\n[dependencies]\n"); + + for (name, dep) in &manifest.dependencies { + match &dep.source { + DependencySource::Registry => { + output.push_str(&format!("{} = \"{}\"\n", name, dep.version)); + } + DependencySource::Git { url, branch } => { + if let Some(b) = branch { + output.push_str(&format!( + "{} = {{ version = \"{}\", git = \"{}\", branch = \"{}\" }}\n", + name, dep.version, url, b + )); + } else { + output.push_str(&format!( + "{} = {{ version = \"{}\", git = \"{}\" }}\n", + name, dep.version, url + )); + } + } + DependencySource::Path { path } => { + output.push_str(&format!( + "{} = {{ version = \"{}\", path = \"{}\" }}\n", + name, dep.version, path.display() + )); + } + } + } + + output +} + +/// Recursively copy a directory +fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), String> { + fs::create_dir_all(dst) + .map_err(|e| format!("Failed to create directory: {}", e))?; + + for entry in fs::read_dir(src).map_err(|e| format!("Failed to read directory: {}", e))? { + let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?; + let path = entry.path(); + let dest_path = dst.join(entry.file_name()); + + if path.is_dir() { + copy_dir_recursive(&path, &dest_path)?; + } else { + fs::copy(&path, &dest_path) + .map_err(|e| format!("Failed to copy file: {}", e))?; + } + } + + Ok(()) +} + +// Platform-specific cache directory handling +mod dirs { + use std::path::PathBuf; + + pub fn cache_dir() -> Option { + #[cfg(target_os = "linux")] + { + std::env::var("XDG_CACHE_HOME") + .map(PathBuf::from) + .ok() + .or_else(|| { + std::env::var("HOME") + .map(|h| PathBuf::from(h).join(".cache")) + .ok() + }) + } + + #[cfg(target_os = "macos")] + { + std::env::var("HOME") + .map(|h| PathBuf::from(h).join("Library").join("Caches")) + .ok() + } + + #[cfg(target_os = "windows")] + { + std::env::var("LOCALAPPDATA") + .map(PathBuf::from) + .ok() + } + + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { + None + } + } +}