Skip to main content

rattler_menuinst/
lib.rs

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
71/// Returns true if the given relative path points to a Menu schema JSON file
72/// within a package (i.e. `Menu/*.json`).
73pub fn is_menu_schema_path(path: &Path) -> bool {
74    path.starts_with("Menu/") && path.extension().is_some_and(|ext| ext == "json")
75}
76
77/// Install menu items for a given prefix record according to `Menu/*.json` files
78/// Note: this function will update the prefix record with the installed menu items
79/// and write it back to the prefix record file if any Menu item is found
80pub fn install_menuitems_for_record(
81    target_prefix: &Path,
82    prefix_record: &PrefixRecord,
83    platform: Platform,
84    menu_mode: MenuMode,
85) -> Result<(), MenuInstError> {
86    // Look for Menu/*.json files in the package paths
87    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        // Store tracker in the prefix record
105        let mut record = prefix_record.clone();
106        record.installed_system_menus = tracker_vec;
107
108        // Save the updated prefix record
109        record.write_to_path(
110            target_prefix.join("conda-meta").join(record.file_name()),
111            true,
112        )?;
113    }
114
115    Ok(())
116}
117
118// Install menu items from a given schema file
119pub 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
179/// Remove all menu items from a given prefix record.
180/// This function will remove the menu items from the system,
181/// and update the prefix record with removing the tracker entries
182pub fn remove_menuitems_for_record(
183    target_prefix: &Path,
184    prefix_record: PrefixRecord,
185) -> Result<(), MenuInstError> {
186    // Remove menu items from the system
187    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    // Save the updated prefix record
193    record.write_to_path(
194        target_prefix.join("conda-meta").join(record.file_name()),
195        true,
196    )?;
197
198    Ok(())
199}
200
201/// Remove menu items from a given schema file
202pub 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}