zoi_transaction/
rollback.rs1use anyhow::{Result, anyhow};
2use colored::*;
3use semver::Version;
4use std::fs;
5use std::path::PathBuf;
6use zoi_core::{sysroot, types, utils as core_utils};
7use zoi_resolver::{local, resolve};
8
9pub fn run(package_name: &str, yes: bool) -> Result<()> {
10 println!("Attempting to roll back '{}'...", package_name.cyan());
11
12 let request = resolve::parse_source_string(package_name)?;
13 let sub_package = request.sub_package.clone();
14 let scope_order = [
15 types::Scope::User,
16 types::Scope::System,
17 types::Scope::Project,
18 ];
19 let mut current_manifest = None;
20 let mut scope = None;
21 for candidate_scope in scope_order {
22 let mut matches = local::find_installed_manifests_matching(&request, candidate_scope)?;
23 match matches.len() {
24 0 => continue,
25 1 => {
26 current_manifest = Some(matches.remove(0));
27 scope = Some(candidate_scope);
28 break;
29 }
30 _ => {
31 return Err(anyhow!(
32 "Package '{}' is ambiguous in {:?} scope. Use an explicit source like '#handle@repo/name[:sub]@version'.",
33 request.name,
34 candidate_scope
35 ));
36 }
37 }
38 }
39
40 let Some(current_manifest) = current_manifest else {
41 return Err(anyhow!("Package '{}' is not installed.", package_name));
42 };
43 let scope = scope.ok_or_else(|| anyhow!("scope should be set when a manifest is found"))?;
44
45 let package_dir = local::get_package_dir(
46 scope,
47 ¤t_manifest.registry_handle,
48 ¤t_manifest.repo,
49 ¤t_manifest.name,
50 )?;
51
52 let mut versions = Vec::new();
53 if let Ok(entries) = fs::read_dir(&package_dir) {
54 for entry in entries.flatten() {
55 let path = entry.path();
56 if path.is_dir()
57 && let Some(version_str) = path.file_name().and_then(|s| s.to_str())
58 && version_str != "latest"
59 && version_str != "dependents"
60 && Version::parse(version_str).is_ok()
61 {
62 versions.push(version_str.to_string());
63 }
64 }
65 }
66 versions.sort();
67
68 if versions.len() < 2 {
69 return Err(anyhow!("No previous version to roll back to."));
70 }
71
72 let current_version = versions
73 .pop()
74 .ok_or_else(|| anyhow!("Failed to get current version from list"))?;
75 let previous_version = versions
76 .pop()
77 .ok_or_else(|| anyhow!("Failed to get previous version from list"))?;
78
79 println!(
80 "Rolling back from version {} to {}",
81 current_version.to_string().yellow(),
82 previous_version.to_string().green()
83 );
84
85 if !core_utils::ask_for_confirmation("This will remove the current version. Continue?", yes) {
86 println!("Operation aborted.");
87 return Ok(());
88 }
89
90 let previous_version_dir = package_dir.join(&previous_version);
91
92 let manifest_filename = if let Some(sub) = &sub_package {
93 format!("manifest-{}.yaml", sub)
94 } else {
95 "manifest.yaml".to_string()
96 };
97 let prev_manifest_path = previous_version_dir.join(&manifest_filename);
98 if !prev_manifest_path.exists() {
99 return Err(anyhow!(
100 "No manifest found for {} in version {}. Rollback not possible.",
101 package_name,
102 previous_version
103 ));
104 }
105
106 let latest_symlink_path = package_dir.join("latest");
107 zoi_core::utils::symlink_dir(&previous_version_dir, &latest_symlink_path)?;
108
109 let content = fs::read_to_string(&prev_manifest_path)?;
110 let prev_manifest: types::InstallManifest = serde_yaml::from_str(&content)?;
111
112 if let Some(bins) = &prev_manifest.bins {
113 let bin_root = get_bin_root(scope)?;
114 for bin in bins {
115 let symlink_path = bin_root.join(bin);
116 create_shim(&symlink_path)?;
117 }
118 } else if prev_manifest.sub_package.is_none() {
119 let symlink_path = get_bin_root(scope)?.join(¤t_manifest.name);
120 create_shim(&symlink_path)?;
121 }
122
123 if let Some(completions) = &prev_manifest.completions {
124 let prev_version_dir = package_dir.join(&previous_version);
125 for completion in completions {
126 let store_path = prev_version_dir
127 .join("shell")
128 .join(&completion.shell)
129 .join(&completion.filename);
130 let completions_root = super::get_completions_root(scope, &completion.shell)?;
131 let pkg_dir = completions_root.join(&prev_manifest.name);
132 let link_path = pkg_dir.join(&completion.filename);
133 if store_path.exists() {
134 let _ = super::create_completion_symlink(&store_path, &link_path);
135 }
136 }
137 }
138
139 let current_version_dir = package_dir.join(¤t_version);
140 let mut has_other_manifests = false;
141 if let Ok(entries) = fs::read_dir(¤t_version_dir) {
142 for entry in entries.flatten() {
143 let name = entry.file_name().to_string_lossy().to_string();
144 if name.starts_with("manifest") && name.ends_with(".yaml") && name != manifest_filename
145 {
146 has_other_manifests = true;
147 break;
148 }
149 }
150 }
151
152 if has_other_manifests {
153 for file_path_str in ¤t_manifest.installed_files {
154 let file_path = PathBuf::from(file_path_str);
155 if file_path.exists() {
156 if file_path.is_dir() {
157 let _ = fs::remove_dir_all(&file_path);
158 } else {
159 let _ = fs::remove_file(&file_path);
160 }
161 }
162 }
163 let current_manifest_path = current_version_dir.join(&manifest_filename);
164 if current_manifest_path.exists() {
165 fs::remove_file(current_manifest_path)?;
166 }
167 } else {
168 fs::remove_dir_all(current_version_dir)?;
169 }
170
171 println!(
172 "Successfully rolled back '{}' to version {}.",
173 package_name.cyan(),
174 previous_version.to_string().green()
175 );
176
177 Ok(())
178}
179
180fn get_bin_root(scope: types::Scope) -> Result<PathBuf> {
181 match scope {
182 types::Scope::User => {
183 let home_dir = core_utils::get_user_home()
184 .ok_or_else(|| anyhow!("Could not find home directory."))?;
185 Ok(sysroot::apply_sysroot(home_dir.join(".zoi/pkgs/bin")))
186 }
187 types::Scope::System => {
188 if cfg!(target_os = "windows") {
189 Ok(sysroot::apply_sysroot(PathBuf::from(
190 "C:\\ProgramData\\zoi\\pkgs\\bin",
191 )))
192 } else {
193 Ok(sysroot::apply_sysroot(PathBuf::from("/usr/local/bin")))
194 }
195 }
196 types::Scope::Project => {
197 let current_dir = std::env::current_dir()?;
198 Ok(current_dir.join(".zoi").join("pkgs").join("bin"))
199 }
200 }
201}
202
203fn create_shim(path: &std::path::Path) -> Result<()> {
204 if cfg!(target_os = "windows") {
205 let binary_path = path.with_extension("exe");
206 if binary_path.exists() {
207 return Ok(());
208 }
209 } else if path.exists() {
210 return Ok(());
211 }
212 Ok(())
213}