1use std::path::{Path, PathBuf};
2
3use rattler_conda_types::{
4 menuinst::{MenuMode, Tracker},
5 Platform, PrefixRecord,
6};
7
8#[cfg(target_os = "linux")]
9mod linux;
10#[cfg(target_os = "macos")]
11mod macos;
12mod render;
13pub mod schema;
14#[cfg(target_os = "windows")]
15mod windows;
16
17use crate::{render::BaseMenuItemPlaceholders, schema::MenuInstSchema};
18
19mod utils;
20
21#[derive(thiserror::Error, Debug)]
22pub enum MenuInstError {
23 #[error("IO error: {0}")]
24 IoError(#[from] std::io::Error),
25
26 #[error("deserialization error: {0}")]
27 SerdeError(#[from] serde_json::Error),
28
29 #[error("failed to install menu item: {0}")]
30 InstallError(String),
31
32 #[error("invalid path: {0}")]
33 InvalidPath(PathBuf),
34
35 #[cfg(target_os = "linux")]
36 #[error("could not quote command with shlex: {0}")]
37 ShlexQuoteError(#[from] shlex::QuoteError),
38
39 #[cfg(target_os = "macos")]
40 #[error("failed to create plist: {0}")]
41 PlistError(#[from] plist::Error),
42
43 #[cfg(target_os = "macos")]
44 #[error("duplicate Info.plist property ({0}) found in `info_plist_extra`")]
45 PlistDuplicateError(String),
46
47 #[cfg(target_os = "macos")]
48 #[error("failed to sign plist: {0}")]
49 SigningFailed(String),
50
51 #[error("failed to install menu item: {0}")]
52 ActivationError(#[from] rattler_shell::activation::ActivationError),
53
54 #[cfg(target_os = "linux")]
55 #[error("failed to install menu item: {0}")]
56 XmlError(#[from] quick_xml::Error),
57
58 #[cfg(target_os = "windows")]
59 #[error("failed to install menu item: {0}")]
60 WindowsError(#[from] ::windows::core::Error),
61
62 #[cfg(target_os = "windows")]
63 #[error("failed to register terminal profile: {0}")]
64 TerminalProfileError(#[from] windows::TerminalUpdateError),
65
66 #[cfg(target_os = "linux")]
67 #[error("menu config location is not a file: {0:?}")]
68 MenuConfigNotAFile(PathBuf),
69}
70
71pub fn is_menu_schema_path(path: &Path) -> bool {
74 path.starts_with("Menu/") && path.extension().is_some_and(|ext| ext == "json")
75}
76
77pub fn install_menuitems_for_record(
81 target_prefix: &Path,
82 prefix_record: &PrefixRecord,
83 platform: Platform,
84 menu_mode: MenuMode,
85) -> Result<(), MenuInstError> {
86 let menu_files: Vec<_> = prefix_record
88 .paths_data
89 .paths
90 .iter()
91 .filter(|path| is_menu_schema_path(&path.relative_path))
92 .collect();
93
94 for menu_file in menu_files {
95 let full_path = target_prefix.join(&menu_file.relative_path);
96 let tracker_vec = install_menuitems(
97 &full_path,
98 target_prefix,
99 target_prefix,
100 platform,
101 menu_mode,
102 )?;
103
104 let mut record = prefix_record.clone();
106 record.installed_system_menus = tracker_vec;
107
108 record.write_to_path(
110 target_prefix.join("conda-meta").join(record.file_name()),
111 true,
112 )?;
113 }
114
115 Ok(())
116}
117
118pub fn install_menuitems(
120 file: &Path,
121 prefix: &Path,
122 base_prefix: &Path,
123 platform: Platform,
124 menu_mode: MenuMode,
125) -> Result<Vec<Tracker>, MenuInstError> {
126 let text = std::fs::read_to_string(file)?;
127 let menu_inst: MenuInstSchema = serde_json::from_str(&text)?;
128 let placeholders = BaseMenuItemPlaceholders::new(base_prefix, prefix, platform);
129
130 let mut trackers = Vec::new();
131 for item in menu_inst.menu_items {
132 if platform.is_linux() {
133 #[cfg(target_os = "linux")]
134 if let Some(linux_item) = item.platforms.linux {
135 let command = item.command.merge(linux_item.base);
136 let linux_tracker = linux::install_menu_item(
137 &menu_inst.menu_name,
138 prefix,
139 linux_item.specific,
140 command,
141 &placeholders,
142 menu_mode,
143 )?;
144 trackers.push(Tracker::Linux(linux_tracker));
145 }
146 } else if platform.is_osx() {
147 #[cfg(target_os = "macos")]
148 if let Some(macos_item) = item.platforms.osx {
149 let command = item.command.merge(macos_item.base);
150 let macos_tracker = macos::install_menu_item(
151 prefix,
152 macos_item.specific,
153 command,
154 &placeholders,
155 menu_mode,
156 )?;
157 trackers.push(Tracker::MacOs(macos_tracker));
158 };
159 } else if platform.is_windows() {
160 #[cfg(target_os = "windows")]
161 if let Some(windows_item) = item.platforms.win {
162 let command = item.command.merge(windows_item.base);
163 let tracker = windows::install_menu_item(
164 &menu_inst.menu_name,
165 prefix,
166 windows_item.specific,
167 command,
168 &placeholders,
169 menu_mode,
170 )?;
171 trackers.push(Tracker::Windows(tracker));
172 }
173 }
174 }
175
176 Ok(trackers)
177}
178
179pub fn remove_menuitems_for_record(
183 target_prefix: &Path,
184 prefix_record: PrefixRecord,
185) -> Result<(), MenuInstError> {
186 remove_menu_items(&prefix_record.installed_system_menus)?;
188
189 let mut record = prefix_record.clone();
190 record.installed_system_menus = Vec::new();
191
192 record.write_to_path(
194 target_prefix.join("conda-meta").join(record.file_name()),
195 true,
196 )?;
197
198 Ok(())
199}
200
201pub fn remove_menu_items(tracker: &Vec<Tracker>) -> Result<(), MenuInstError> {
203 for el in tracker {
204 #[allow(unused)]
205 match el {
206 Tracker::MacOs(tracker) => {
207 #[cfg(target_os = "macos")]
208 macos::remove_menu_item(tracker)?;
209 }
210 Tracker::Linux(tracker) => {
211 #[cfg(target_os = "linux")]
212 linux::remove_menu_item(tracker)?;
213 }
214 Tracker::Windows(tracker) => {
215 #[cfg(target_os = "windows")]
216 windows::remove_menu_item(tracker)?;
217 }
218 }
219 }
220
221 Ok(())
222}
223
224#[cfg(test)]
225pub mod test {
226 use std::path::{Path, PathBuf};
227
228 #[allow(dead_code)]
229 pub(crate) fn test_data() -> PathBuf {
230 Path::new(env!("CARGO_MANIFEST_DIR")).join("test-data")
231 }
232}