feat: implement package manager

Adds dependency management for Lux projects:
- `lux pkg install` - Install all dependencies from lux.toml
- `lux pkg add <pkg>` - Add a dependency (supports --git, --path)
- `lux pkg remove <pkg>` - Remove a dependency
- `lux pkg list` - List dependencies with install status
- `lux pkg update` - Update all dependencies
- `lux pkg clean` - Remove installed packages

Supports three dependency sources:
- Registry (placeholder for future package registry)
- Git repositories (with optional branch)
- Local file paths

Packages are installed to .lux_packages/ in the project root.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 10:44:49 -05:00
parent 1c59fdd735
commit a037f5bd2f
2 changed files with 777 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ mod interpreter;
mod lexer; mod lexer;
mod lsp; mod lsp;
mod modules; mod modules;
mod package;
mod parser; mod parser;
mod schema; mod schema;
mod typechecker; mod typechecker;
@@ -133,6 +134,10 @@ fn main() {
std::process::exit(1); std::process::exit(1);
} }
} }
"pkg" => {
// Package manager
handle_pkg_command(&args[2..]);
}
path => { path => {
// Run a file // Run a file
run_file(path); run_file(path);
@@ -156,6 +161,7 @@ fn print_help() {
println!(" lux watch <file.lux> Watch and re-run on changes"); println!(" lux watch <file.lux> Watch and re-run on changes");
println!(" lux debug <file.lux> Start interactive debugger"); println!(" lux debug <file.lux> Start interactive debugger");
println!(" lux init [name] Initialize a new project"); println!(" lux init [name] Initialize a new project");
println!(" lux pkg <command> Package manager (install, add, remove, list, update)");
println!(" lux --lsp Start LSP server (for IDE integration)"); println!(" lux --lsp Start LSP server (for IDE integration)");
println!(" lux --help Show this help"); println!(" lux --help Show this help");
println!(" lux --version Show version"); 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 <package> [version] [--git <url>] [--path <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 <package>");
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 <command> [options]");
println!();
println!("Commands:");
println!(" install, i Install all dependencies from lux.toml");
println!(" add <pkg> Add a dependency");
println!(" Options:");
println!(" [version] Specify version (default: 0.0.0)");
println!(" --git <url> Install from git repository");
println!(" --branch <name> Git branch (with --git)");
println!(" --path <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>) { fn init_project(name: Option<&str>) {
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;

621
src/package.rs Normal file
View File

@@ -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<String, Dependency>,
}
/// Project information from lux.toml
#[derive(Debug, Clone)]
pub struct ProjectInfo {
pub name: String,
pub version: String,
pub description: Option<String>,
pub authors: Vec<String>,
pub license: Option<String>,
}
/// 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<String> },
/// 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<PathBuf> {
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<Manifest, String> {
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<PathBuf> {
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<Manifest, String> {
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<Dependency, String> {
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<String> = 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<PathBuf> {
#[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
}
}
}