fusabi_pm/
install.rs

1//! Install command for cloning git dependencies.
2
3use crate::manifest::{Dependency, Manifest, ManifestError};
4use crate::registry::{Registry, RegistryError};
5use git2::Repository;
6use std::path::Path;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum InstallError {
11    #[error("Manifest error: {0}")]
12    Manifest(#[from] ManifestError),
13
14    #[error("Git error: {0}")]
15    Git(#[from] git2::Error),
16
17    #[error("IO error: {0}")]
18    Io(#[from] std::io::Error),
19
20    #[error("Registry error: {0}")]
21    Registry(#[from] RegistryError),
22
23    #[error("{0}")]
24    Other(String),
25}
26
27pub fn install_dependencies(project_dir: &Path) -> Result<(), InstallError> {
28    let manifest_path = project_dir.join("fusabi.toml");
29    if !manifest_path.exists() {
30        return Err(InstallError::Other(
31            "fusabi.toml not found. Run 'fpm init' first.".to_string(),
32        ));
33    }
34
35    let manifest = Manifest::load(&manifest_path)?;
36    let packages_dir = project_dir.join("fusabi_packages");
37
38    if manifest.dependencies.is_empty() {
39        println!("No dependencies to install.");
40        return Ok(());
41    }
42
43    std::fs::create_dir_all(&packages_dir)?;
44
45    let registry = Registry::new();
46
47    for (name, dependency) in &manifest.dependencies {
48        match dependency {
49            Dependency::Detailed(detailed) => {
50                if let Some(git_url) = &detailed.git {
51                    install_git_dependency(name, git_url, detailed.rev.as_deref(), &packages_dir)?;
52                } else if detailed.path.is_some() {
53                    println!("Skipping local path dependency '{}'", name);
54                } else if let Some(version) = &detailed.version {
55                    install_registry_dependency(name, version, &packages_dir, &registry)?;
56                } else {
57                    println!(
58                        "Skipping dependency '{}': no git, path, or version specified",
59                        name
60                    );
61                }
62            }
63            Dependency::Simple(version) => {
64                install_registry_dependency(name, version, &packages_dir, &registry)?;
65            }
66        }
67    }
68
69    println!("Install complete.");
70    Ok(())
71}
72
73fn install_git_dependency(
74    name: &str,
75    git_url: &str,
76    rev: Option<&str>,
77    packages_dir: &Path,
78) -> Result<(), InstallError> {
79    let dep_path = packages_dir.join(name);
80
81    if dep_path.exists() {
82        println!("Dependency '{}' already exists, skipping.", name);
83        return Ok(());
84    }
85
86    println!("Cloning '{}'...", name);
87
88    let repo = Repository::clone(git_url, &dep_path)?;
89
90    if let Some(rev) = rev {
91        checkout_rev(&repo, rev)?;
92    }
93
94    println!("Installed '{}'", name);
95    Ok(())
96}
97
98fn install_registry_dependency(
99    name: &str,
100    version_constraint: &str,
101    packages_dir: &Path,
102    registry: &Registry,
103) -> Result<(), InstallError> {
104    let dep_path = packages_dir.join(name);
105
106    if dep_path.exists() {
107        println!("Dependency '{}' already exists, skipping.", name);
108        return Ok(());
109    }
110
111    println!("Resolving '{}' ({})...", name, version_constraint);
112
113    let resolved = registry.resolve(name, version_constraint)?;
114
115    println!(
116        "Installing '{}' v{} from {}",
117        resolved.name, resolved.version, resolved.git_url
118    );
119
120    let repo = Repository::clone(&resolved.git_url, &dep_path)?;
121
122    if let Some(ref rev) = resolved.rev {
123        if let Err(e) = checkout_rev(&repo, rev) {
124            println!(
125                "Warning: Could not checkout version tag '{}': {}. Using default branch.",
126                rev, e
127            );
128        }
129    }
130
131    println!("Installed '{}' v{}", resolved.name, resolved.version);
132    Ok(())
133}
134
135fn checkout_rev(repo: &Repository, rev: &str) -> Result<(), git2::Error> {
136    let (object, reference) = repo.revparse_ext(rev)?;
137    repo.checkout_tree(&object, None)?;
138
139    match reference {
140        Some(gref) => repo.set_head(gref.name().unwrap()),
141        None => repo.set_head_detached(object.id()),
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use tempfile::TempDir;
149
150    #[test]
151    fn test_install_no_manifest() {
152        let temp_dir = TempDir::new().unwrap();
153        let result = install_dependencies(temp_dir.path());
154        assert!(result.is_err());
155    }
156
157    #[test]
158    fn test_install_empty_dependencies() {
159        let temp_dir = TempDir::new().unwrap();
160        let manifest_content = r#"
161[package]
162name = "test"
163version = "0.1.0"
164"#;
165        std::fs::write(temp_dir.path().join("fusabi.toml"), manifest_content).unwrap();
166
167        let result = install_dependencies(temp_dir.path());
168        assert!(result.is_ok());
169    }
170}