1use 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, ®istry)?;
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, ®istry)?;
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}