diff --git a/Cargo.toml b/Cargo.toml index 73d00f1cc..acd0b6b8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,6 +155,7 @@ trybuild = { version = "1.0.101" } typed-path = { version = "0.9.3" } url = { version = "2.5.2" } uuid = { version = "1.11.0", default-features = false } +unicode-normalization = { version = "0.1.24" } walkdir = "2.5.0" windows-sys = { version = "0.59.0", default-features = false } zip = { version = "2.2.0", default-features = false } diff --git a/crates/rattler/Cargo.toml b/crates/rattler/Cargo.toml index 64ade70c7..9a42d19fc 100644 --- a/crates/rattler/Cargo.toml +++ b/crates/rattler/Cargo.toml @@ -33,6 +33,7 @@ memmap2 = { workspace = true } once_cell = { workspace = true } parking_lot = { workspace = true } rattler_cache = { path = "../rattler_cache", version = "0.2.8", default-features = false } +rattler_menuinst = { path = "../rattler_menuinst", version = "0.1.0", default-features = false } rattler_conda_types = { path = "../rattler_conda_types", version = "0.29.0", default-features = false } rattler_digest = { path = "../rattler_digest", version = "1.0.3", default-features = false } rattler_networking = { path = "../rattler_networking", version = "0.21.5", default-features = false } diff --git a/crates/rattler/src/install/driver.rs b/crates/rattler/src/install/driver.rs index 73be08f68..dbaed18e9 100644 --- a/crates/rattler/src/install/driver.rs +++ b/crates/rattler/src/install/driver.rs @@ -7,7 +7,7 @@ use std::{ use indexmap::IndexSet; use itertools::Itertools; -use rattler_conda_types::{prefix_record::PathType, PackageRecord, PrefixRecord}; +use rattler_conda_types::{prefix_record::PathType, PackageRecord, Platform, PrefixRecord}; use simple_spawn_blocking::{tokio::run_blocking_task, Cancelled}; use thiserror::Error; use tokio::sync::{AcquireError, OwnedSemaphorePermit, Semaphore}; @@ -222,6 +222,43 @@ impl InstallDriver { None }; + // find all files in `$PREFIX/menu/*.json` and install them with `menuinst` + if let Ok(read_dir) = target_prefix.join("menu").read_dir() { + for file in read_dir.flatten() { + let file = file.path(); + if file.is_file() && file.extension().map_or(false, |ext| ext == "json") { + tracing::info!("Installing menu item: {:?}", file); + rattler_menuinst::install_menuitems( + &file, + target_prefix, + target_prefix, + &Platform::current(), + ) + .unwrap_or_else(|e| { + tracing::warn!("Failed to install menu item: {} (ignored)", e); + }); + } + } + } + + // find all files in `$PREFIX/menu/*.json` and install them with `menuinst` + if let Ok(read_dir) = target_prefix.join("Menu").read_dir() { + for file in read_dir.flatten() { + let file = file.path(); + if file.is_file() && file.extension().map_or(false, |ext| ext == "json") { + rattler_menuinst::install_menuitems( + &file, + target_prefix, + target_prefix, + &Platform::current(), + ) + .unwrap_or_else(|e| { + tracing::warn!("Failed to install menu item: {} (ignored)", e); + }); + } + } + } + Ok(PostProcessResult { post_link_result, clobbered_paths, diff --git a/crates/rattler_menuinst/Cargo.toml b/crates/rattler_menuinst/Cargo.toml index 3629708c7..d2ce68f16 100644 --- a/crates/rattler_menuinst/Cargo.toml +++ b/crates/rattler_menuinst/Cargo.toml @@ -16,14 +16,13 @@ dirs = { workspace = true } serde = { workspace = true, features = ["derive"] } shlex = { workspace = true } serde_json = { workspace = true } -sha1 = "0.10.6" -tracing.workspace = true -rattler_conda_types = { path="../rattler_conda_types", version = "0.27.2", default-features = false } +tracing = { workspace = true } +rattler_conda_types = { path = "../rattler_conda_types", default-features = false } thiserror = { workspace = true } -unicode-normalization = "0.1.24" -regex.workspace = true -tempfile.workspace = true -fs-err = "2.11.0" +unicode-normalization = { workspace = true } +regex = { workspace = true } +tempfile = { workspace = true } +fs-err = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] winapi = "0.3.9" diff --git a/crates/rattler_menuinst/src/lib.rs b/crates/rattler_menuinst/src/lib.rs index d334e345c..bb5f11c5a 100644 --- a/crates/rattler_menuinst/src/lib.rs +++ b/crates/rattler_menuinst/src/lib.rs @@ -4,10 +4,10 @@ use rattler_conda_types::Platform; mod linux; mod macos; -#[cfg(target_os = "windows")] -mod windows; mod render; mod schema; +#[cfg(target_os = "windows")] +mod windows; pub mod slugify; pub use slugify::slugify; diff --git a/crates/rattler_menuinst/src/linux.rs b/crates/rattler_menuinst/src/linux.rs index 53cdfc390..ff4845c05 100644 --- a/crates/rattler_menuinst/src/linux.rs +++ b/crates/rattler_menuinst/src/linux.rs @@ -24,6 +24,7 @@ use crate::MenuMode; use crate::{schema::Linux, MenuInstError}; pub struct LinuxMenu { + name: String, item: Linux, directories: Directories, } @@ -70,6 +71,10 @@ impl Directories { impl LinuxMenu { fn new(item: Linux, mode: MenuMode) -> Self { LinuxMenu { + name: item + .base + .get_name(crate::schema::Environment::Base) + .to_string(), item, directories: Directories::new(mode), } @@ -77,7 +82,7 @@ impl LinuxMenu { fn location(&self) -> PathBuf { // TODO: The Python implementation uses one more variable - let filename = format!("{}.desktop", self.item.base.name); + let filename = format!("{}.desktop", self.name); self.directories.desktop_entries_location.join(filename) } @@ -151,7 +156,7 @@ impl LinuxMenu { writeln!(writer, "[Desktop Entry]")?; writeln!(writer, "Type=Application")?; writeln!(writer, "Encoding=UTF-8")?; - writeln!(writer, "Name={}", self.item.base.name)?; + writeln!(writer, "Name={:?}", self.item.base.name)?; writeln!(writer, "Exec={}", self.item.base.command.join(" "))?; writeln!( writer, diff --git a/crates/rattler_menuinst/src/macos.rs b/crates/rattler_menuinst/src/macos.rs index c4d0deb9b..4db8be2fe 100644 --- a/crates/rattler_menuinst/src/macos.rs +++ b/crates/rattler_menuinst/src/macos.rs @@ -2,7 +2,6 @@ use crate::{schema::MacOS, slugify, utils, MenuInstError, MenuMode}; use fs_err as fs; use fs_err::File; use plist::Value; -use sha1::{Digest, Sha1}; use std::{ io::{BufWriter, Write}, os::unix::fs::PermissionsExt, @@ -11,6 +10,7 @@ use std::{ }; pub struct MacOSMenu { + name: String, prefix: PathBuf, item: MacOS, directories: Directories, @@ -63,6 +63,10 @@ impl Directories { impl MacOSMenu { pub fn new(prefix: &Path, item: MacOS, directories: Directories) -> Self { Self { + name: item + .base + .get_name(crate::schema::Environment::Base) + .to_string(), prefix: prefix.to_path_buf(), item, directories, @@ -111,7 +115,7 @@ impl MacOSMenu { f.write_all(format!("APPL{short_name}").as_bytes())?; Ok(()) }; - let short_name = slugify(&self.item.base.name.chars().take(8).collect::()); + let short_name = slugify(&self.name.chars().take(8).collect::()); create_pkg_info(&self.directories.location, &short_name)?; if self.needs_appkit_launcher() { @@ -122,11 +126,12 @@ impl MacOSMenu { } fn write_plist_info(&self) -> Result<(), MenuInstError> { - let name = self.item.base.name.clone(); + let name = self.name.clone(); let slugname = slugify(&name); let shortname = if slugname.len() > 16 { - let hashed = format!("{:x}", Sha1::digest(slugname.as_bytes())); + // let hashed = format!("{:x}", Sha256::digest(slugname.as_bytes())); + let hashed = "123456"; format!("{}{}", &slugname[..10], &hashed[..6]) } else { slugname.clone() @@ -158,7 +163,13 @@ impl MacOSMenu { } if self.needs_appkit_launcher() { - println!("Writing plist to {}", self.directories.nested_location.join("Contents/Info.plist").display()); + println!( + "Writing plist to {}", + self.directories + .nested_location + .join("Contents/Info.plist") + .display() + ); plist::to_file_xml( self.directories.nested_location.join("Contents/Info.plist"), &pl, @@ -181,13 +192,13 @@ impl MacOSMenu { pl.insert("LSBackgroundOnly".into(), Value::Boolean(background_only)); } - self.item.ls_environment.as_ref().map(|env| { + if let Some(env) = self.item.ls_environment.as_ref() { let mut env_dict = plist::Dictionary::new(); for (k, v) in env { env_dict.insert(k.into(), Value::String(v.into())); } pl.insert("LSEnvironment".into(), Value::Dictionary(env_dict)); - }); + } if let Some(version) = self.item.ls_minimum_system_version.as_ref() { pl.insert( @@ -239,29 +250,35 @@ impl MacOSMenu { // pl.insert("UTExportedTypeDeclarations".into(), Value::Array(type_array)); }); - self.item - .ut_imported_type_declarations - .as_ref() - .map(|_types| { - // let mut type_array = Vec::new(); - // for t in types { - // let mut type_dict = plist::Dictionary::new(); - // type_dict.insert("UTTypeConformsTo".into(), Value::Array(t.ut_type_conforms_to.iter().map(|s| Value::String(s.clone())).collect())); - // type_dict.insert("UTTypeDescription".into(), Value::String(t.ut_type_description.clone().unwrap_or_default())); - // type_dict.insert("UTTypeIconFile".into(), Value::String(t.ut_type_icon_file.clone().unwrap_or_default())); - // type_dict.insert("UTTypeIdentifier".into(), Value::String(t.ut_type_identifier.clone())); - // type_dict.insert("UTTypeReferenceURL".into(), Value::String(t.ut_type_reference_url.clone().unwrap_or_default())); - // let mut tag_spec = plist::Dictionary::new(); - // for (k, v) in &t.ut_type_tag_specification { - // tag_spec.insert(k.clone(), Value::Array(v.iter().map(|s| Value::String(s.clone())).collect())); - // } - // type_dict.insert("UTTypeTagSpecification".into(), Value::Dictionary(tag_spec)); - // type_array.push(Value::Dictionary(type_dict)); - // } - // pl.insert("UTImportedTypeDeclarations".into(), Value::Array(type_array)); - }); - - println!("Writing plist to {}", self.directories.location.join("Contents/Info.plist").display()); + // self.item + // .ut_imported_type_declarations + // .as_ref() + // .map(|_types| { + // // let mut type_array = Vec::new(); + // // for t in types { + // // let mut type_dict = plist::Dictionary::new(); + // // type_dict.insert("UTTypeConformsTo".into(), Value::Array(t.ut_type_conforms_to.iter().map(|s| Value::String(s.clone())).collect())); + // // type_dict.insert("UTTypeDescription".into(), Value::String(t.ut_type_description.clone().unwrap_or_default())); + // // type_dict.insert("UTTypeIconFile".into(), Value::String(t.ut_type_icon_file.clone().unwrap_or_default())); + // // type_dict.insert("UTTypeIdentifier".into(), Value::String(t.ut_type_identifier.clone())); + // // type_dict.insert("UTTypeReferenceURL".into(), Value::String(t.ut_type_reference_url.clone().unwrap_or_default())); + // // let mut tag_spec = plist::Dictionary::new(); + // // for (k, v) in &t.ut_type_tag_specification { + // // tag_spec.insert(k.clone(), Value::Array(v.iter().map(|s| Value::String(s.clone())).collect())); + // // } + // // type_dict.insert("UTTypeTagSpecification".into(), Value::Dictionary(tag_spec)); + // // type_array.push(Value::Dictionary(type_dict)); + // // } + // // pl.insert("UTImportedTypeDeclarations".into(), Value::Array(type_array)); + // }); + + println!( + "Writing plist to {}", + self.directories + .location + .join("Contents/Info.plist") + .display() + ); plist::to_file_xml(self.directories.location.join("Contents/Info.plist"), &pl)?; Ok(()) @@ -296,7 +313,7 @@ impl MacOSMenu { .arg("--options") .arg("runtime") .arg("--prefix") - .arg(format!("com.{}", slugify(&self.item.base.name))) + .arg(format!("com.{}", slugify(&self.name))) .arg("--entitlements") .arg(&entitlements_file) .arg(self.directories.location.to_str().unwrap()) @@ -441,12 +458,12 @@ impl MacOSMenu { } fn default_appkit_launcher_path(&self) -> PathBuf { - let name = slugify(&self.item.base.name); + let name = slugify(&self.name); self.directories.location.join("Contents/MacOS").join(&name) } fn default_launcher_path(&self) -> PathBuf { - let name = slugify(&self.item.base.name); + let name = slugify(&self.name); if self.needs_appkit_launcher() { self.directories .nested_location @@ -463,9 +480,9 @@ impl MacOSMenu { } if register { - self.lsregister(&["-R", self.directories.location.to_str().unwrap()]) + Self::lsregister(&["-R", self.directories.location.to_str().unwrap()]) } else { - self.lsregister(&[ + Self::lsregister(&[ "-R", "-u", "-all", @@ -474,7 +491,7 @@ impl MacOSMenu { } } - fn lsregister(&self, args: &[&str]) -> Result<(), MenuInstError> { + fn lsregister(args: &[&str]) -> Result<(), MenuInstError> { let exe = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"; let output = Command::new(exe).args(args).output().map_err(|e| { @@ -523,7 +540,7 @@ pub(crate) fn install_menu_item( ) -> Result<(), MenuInstError> { let bundle_name = macos_item.cf_bundle_name.as_ref().unwrap(); let directories = Directories::new(menu_mode, bundle_name); - println!("Installing menu item for {}", bundle_name); + println!("Installing menu item for {bundle_name}"); let menu = MacOSMenu::new(prefix, macos_item, directories); menu.install() } diff --git a/crates/rattler_menuinst/src/schema.rs b/crates/rattler_menuinst/src/schema.rs index ab7fb83e0..23e9047ff 100644 --- a/crates/rattler_menuinst/src/schema.rs +++ b/crates/rattler_menuinst/src/schema.rs @@ -12,7 +12,7 @@ pub struct MenuItemNameDict { #[serde(deny_unknown_fields)] pub struct BasePlatformSpecific { #[serde(default)] - pub name: String, + pub name: Option, #[serde(default)] pub description: String, pub icon: Option, @@ -25,12 +25,42 @@ pub struct BasePlatformSpecific { pub terminal: Option, } +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum NameField { + Simple(String), + Complex(NameComplex), +} + +impl BasePlatformSpecific { + pub fn get_name(&self, env: Environment) -> &str { + match self.name.as_ref().unwrap() { + NameField::Simple(name) => name, + NameField::Complex(complex_name) => match env { + Environment::Base => &complex_name.target_environment_is_base, + Environment::NotBase => &complex_name.target_environment_is_not_base, + }, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct NameComplex { + pub target_environment_is_base: String, + pub target_environment_is_not_base: String, +} + +pub enum Environment { + Base, + NotBase, +} + impl BasePlatformSpecific { pub fn merge_parent(self, parent: &MenuItem) -> Self { - let name = if self.name.is_empty() { + let name = if self.name.is_none() { parent.name.clone() } else { - self.name + self.name.unwrap() }; let description = if self.description.is_empty() { @@ -46,7 +76,7 @@ impl BasePlatformSpecific { }; BasePlatformSpecific { - name, + name: Some(name), description, icon: self.icon.or(parent.icon.clone()), command, @@ -206,7 +236,7 @@ pub struct Platforms { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(deny_unknown_fields)] pub struct MenuItem { - pub name: String, + pub name: NameField, pub description: String, pub command: Vec, pub icon: Option, @@ -233,6 +263,8 @@ pub struct MenuInstSchema { mod test { use std::path::{Path, PathBuf}; + use crate::macos::Directories; + pub(crate) fn test_data() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/menuinst") } @@ -263,4 +295,25 @@ mod test { let schema: super::MenuInstSchema = serde_json::from_str(&schema_str).unwrap(); insta::assert_debug_snapshot!(schema); } + + #[test] + fn test_deserialize_spyder() { + let test_data = test_data(); + let schema_path = test_data.join("spyder/menu.json"); + let schema_str = std::fs::read_to_string(schema_path).unwrap(); + let schema: super::MenuInstSchema = serde_json::from_str(&schema_str).unwrap(); + + let item = schema.menu_items[0].clone(); + let mut macos_item = item.platforms.osx.clone().unwrap(); + let base_item = macos_item.base.merge_parent(&item); + macos_item.base = base_item; + + assert_eq!( + macos_item.base.get_name(super::Environment::Base), + "superspyder 1.2 (base)" + ); + + // let foo = menu_0.platforms.osx.as_ref().unwrap().base.get_name(super::Environment::Base); + insta::assert_debug_snapshot!(schema); + } } diff --git a/crates/rattler_menuinst/src/snapshots/rattler_menuinst__schema__test__deserialize_spyder.snap b/crates/rattler_menuinst/src/snapshots/rattler_menuinst__schema__test__deserialize_spyder.snap new file mode 100644 index 000000000..dca295bb3 --- /dev/null +++ b/crates/rattler_menuinst/src/snapshots/rattler_menuinst__schema__test__deserialize_spyder.snap @@ -0,0 +1,280 @@ +--- +source: crates/rattler_menuinst/src/schema.rs +expression: schema +--- +MenuInstSchema { + id: "https://schemas.conda.io/menuinst-1.schema.json", + schema: "https://json-schema.org/draft-07/schema", + menu_name: "superspyder", + menu_items: [ + MenuItem { + name: Complex( + NameComplex { + target_environment_is_base: "superspyder 1.2 (base)", + target_environment_is_not_base: "superspyder 1.2 ({{ ENV_NAME }})", + }, + ), + description: "Scientific PYthon Development EnviRonment", + command: [ + "", + ], + icon: Some( + "{{ MENU_DIR }}/superspyder.{{ ICON_EXT }}", + ), + precommand: None, + precreate: None, + working_dir: None, + activate: Some( + false, + ), + terminal: Some( + false, + ), + platforms: Platforms { + linux: Some( + Linux { + base: BasePlatformSpecific { + name: None, + description: "", + icon: None, + command: [ + "{{ PREFIX }}/bin/spyder", + "%F", + ], + working_dir: None, + precommand: None, + precreate: None, + activate: None, + terminal: None, + }, + categories: Some( + [ + "Development", + "Science", + ], + ), + dbus_activatable: None, + generic_name: None, + hidden: None, + implements: None, + keywords: None, + mime_type: Some( + [ + "text/x-python", + ], + ), + no_display: None, + not_show_in: None, + only_show_in: None, + prefers_non_default_gpu: None, + startup_notify: None, + startup_wm_class: Some( + "Spyder-__PKG_MAJOR_VER__.{{ ENV_NAME }}", + ), + try_exec: None, + glob_patterns: None, + }, + ), + osx: Some( + MacOS { + base: BasePlatformSpecific { + name: None, + description: "", + icon: None, + command: [ + "./python", + "{{ PREFIX }}/bin/spyder", + "$@", + ], + working_dir: None, + precommand: Some( + "pushd \"$(dirname \"$0\")\" &>/dev/null", + ), + precreate: None, + activate: None, + terminal: None, + }, + cf_bundle_display_name: None, + cf_bundle_identifier: Some( + "org.superspyder-ide.Superspyder-__PKG_MAJOR_VER__-__CFBID_ENV__", + ), + cf_bundle_name: Some( + "Superspyder __PKG_MAJOR_VER__", + ), + cf_bundle_spoken_name: None, + cf_bundle_version: Some( + "__PKG_VERSION__", + ), + cf_bundle_url_types: None, + cf_bundle_document_types: Some( + [ + CFBundleDocumentTypesModel { + cf_bundle_type_icon_file: Some( + "superspyder.icns", + ), + cf_bundle_type_name: "text document", + cf_bundle_type_role: Some( + "Editor", + ), + ls_item_content_types: [ + "com.apple.applescript.text", + "com.apple.ascii-property-list", + "com.apple.audio-unit-preset", + "com.apple.binary-property-list", + "com.apple.configprofile", + "com.apple.crashreport", + "com.apple.dashcode.css", + "com.apple.dashcode.javascript", + "com.apple.dashcode.json", + "com.apple.dashcode.manifest", + "com.apple.dt.document.ascii-property-list", + "com.apple.dt.document.script-suite-property-list", + "com.apple.dt.document.script-terminology-property-list", + "com.apple.property-list", + "com.apple.rez-source", + "com.apple.scripting-definition", + "com.apple.structured-text", + "com.apple.traditional-mac-plain-text", + "com.apple.xcode.ada-source", + "com.apple.xcode.apinotes", + "com.apple.xcode.bash-script", + "com.apple.xcode.configsettings", + "com.apple.xcode.csh-script", + "com.apple.xcode.entitlements-property-list", + "com.apple.xcode.fortran-source", + "com.apple.xcode.glsl-source", + "com.apple.xcode.ksh-script", + "com.apple.xcode.lex-source", + "com.apple.xcode.make-script", + "com.apple.xcode.mig-source", + "com.apple.xcode.pascal-source", + "com.apple.xcode.strings-text", + "com.apple.xcode.tcsh-script", + "com.apple.xcode.yacc-source", + "com.apple.xcode.zsh-script", + "com.apple.xml-property-list", + "com.netscape.javascript-source", + "com.scenarist.closed-caption", + "com.sun.java-source", + "com.sun.java-web-start", + "net.daringfireball.markdown", + "org.khronos.glsl-source", + "org.oasis-open.xliff", + "public.ada-source", + "public.assembly-source", + "public.bash-script", + "public.c-header", + "public.c-plus-plus-header", + "public.c-plus-plus-source", + "public.c-source", + "public.case-insensitive-text", + "public.comma-separated-values-text", + "public.csh-script", + "public.css", + "public.delimited-values-text", + "public.dylan-source", + "public.filename-extension", + "public.fortran-77-source", + "public.fortran-90-source", + "public.fortran-95-source", + "public.fortran-source", + "public.html", + "public.json", + "public.ksh-script", + "public.lex-source", + "public.log", + "public.m3u-playlist", + "public.make-source", + "public.mig-source", + "public.mime-type", + "public.module-map", + "public.nasm-assembly-source", + "public.objective-c-plus-plus-source", + "public.objective-c-source", + "public.opencl-source", + "public.pascal-source", + "public.patch-file", + "public.perl-script", + "public.php-script", + "public.plain-text", + "public.python-script", + "public.rss", + "public.ruby-script", + "public.script", + "public.shell-script", + "public.source-code", + "public.tcsh-script", + "public.text", + "public.utf16-external-plain-text", + "public.utf16-plain-text", + "public.utf8-plain-text", + "public.utf8-tab-separated-values-text", + "public.xhtml", + "public.xml", + "public.yacc-source", + "public.yaml", + "public.zsh-script", + ], + ls_handler_rank: "Default", + }, + ], + ), + ls_application_category_type: None, + ls_background_only: None, + ls_environment: None, + ls_minimum_system_version: None, + ls_multiple_instances_prohibited: None, + ls_requires_native_execution: None, + ns_supports_automatic_graphics_switching: None, + ut_exported_type_declarations: None, + ut_imported_type_declarations: None, + entitlements: None, + link_in_bundle: Some( + { + "{{ PREFIX }}/bin/python": "{{ MENU_ITEM_LOCATION }}/Contents/MacOS/python", + }, + ), + event_handler: None, + }, + ), + win: Some( + Windows { + base: BasePlatformSpecific { + name: None, + description: "", + icon: None, + command: [ + "{{ PREFIX }}/Scripts/superspyder.exe", + "%*", + ], + working_dir: None, + precommand: None, + precreate: None, + activate: None, + terminal: None, + }, + desktop: Some( + true, + ), + quicklaunch: None, + terminal_profile: None, + url_protocols: None, + file_extensions: Some( + [ + ".enaml", + ".ipy", + ".py", + ".pyi", + ".pyw", + ".pyx", + ], + ), + app_user_model_id: Some( + "superspyder-ide.superspyder-__PKG_MAJOR_VER__.{{ ENV_NAME }}", + ), + }, + ), + }, + }, + ], +} diff --git a/crates/rattler_menuinst/src/windows/knownfolders.rs b/crates/rattler_menuinst/src/windows/knownfolders.rs index 3801cf30f..09f7cc9d2 100644 --- a/crates/rattler_menuinst/src/windows/knownfolders.rs +++ b/crates/rattler_menuinst/src/windows/knownfolders.rs @@ -39,11 +39,20 @@ impl Folders { } } - pub fn get_folder_path(&self, key: &str, user_handle: UserHandle) -> Result { + pub fn get_folder_path( + &self, + key: &str, + user_handle: UserHandle, + ) -> Result { self.folder_path(user_handle, true, key) } - fn folder_path(&self, preferred_mode: UserHandle, check_other_mode: bool, key: &str) -> Result { + fn folder_path( + &self, + preferred_mode: UserHandle, + check_other_mode: bool, + key: &str, + ) -> Result { let (preferred_folders, other_folders) = match preferred_mode { UserHandle::Current => (&self.user_folders, &self.system_folders), UserHandle::Common => (&self.system_folders, &self.user_folders), diff --git a/crates/rattler_menuinst/src/windows/registry.rs b/crates/rattler_menuinst/src/windows/registry.rs index d255f0f95..34d4bfe9e 100644 --- a/crates/rattler_menuinst/src/windows/registry.rs +++ b/crates/rattler_menuinst/src/windows/registry.rs @@ -15,7 +15,8 @@ pub fn register_file_extension( HKEY_CURRENT_USER }; - let classes = RegKey::predef(hkey).open_subkey_with_flags("Software\\Classes", KEY_ALL_ACCESS)?; + let classes = + RegKey::predef(hkey).open_subkey_with_flags("Software\\Classes", KEY_ALL_ACCESS)?; // Associate extension with handler let ext_key = classes.create_subkey(&format!("{}\\OpenWithProgids", extension))?; @@ -24,7 +25,10 @@ pub fn register_file_extension( // Register the handler let handler_desc = format!("{} {} handler", extension, identifier); - classes.create_subkey(identifier)?.0.set_value("", &handler_desc)?; + classes + .create_subkey(identifier)? + .0 + .set_value("", &handler_desc)?; tracing::debug!("Created registry entry for handler '{}'", identifier); // Set the 'open' command @@ -49,16 +53,15 @@ pub fn unregister_file_extension(extension: &str, identifier: &str, mode: &str) HKEY_CURRENT_USER }; - let classes = RegKey::predef(hkey).open_subkey_with_flags("Software\\Classes", KEY_ALL_ACCESS)?; + let classes = + RegKey::predef(hkey).open_subkey_with_flags("Software\\Classes", KEY_ALL_ACCESS)?; // Delete the identifier key classes.delete_subkey_all(identifier)?; // Remove the association in OpenWithProgids - let ext_key = classes.open_subkey_with_flags( - &format!("{}\\OpenWithProgids", extension), - KEY_ALL_ACCESS, - ); + let ext_key = + classes.open_subkey_with_flags(&format!("{}\\OpenWithProgids", extension), KEY_ALL_ACCESS); match ext_key { Ok(key) => { @@ -72,11 +75,7 @@ pub fn unregister_file_extension(extension: &str, identifier: &str, mode: &str) } } Err(e) => { - tracing::error!( - "Could not check key '{}' for deletion: {}", - extension, - e - ); + tracing::error!("Could not check key '{}' for deletion: {}", extension, e); return Err(e.into()); } } @@ -94,10 +93,12 @@ pub fn register_url_protocol( let key = if mode == "system" { RegKey::predef(HKEY_CLASSES_ROOT).create_subkey(protocol)? } else { - RegKey::predef(HKEY_CURRENT_USER).create_subkey(&format!("Software\\Classes\\{}", protocol))? + RegKey::predef(HKEY_CURRENT_USER) + .create_subkey(&format!("Software\\Classes\\{}", protocol))? }; - key.0.set_value("", &format!("URL:{}", protocol.to_uppercase()))?; + key.0 + .set_value("", &format!("URL:{}", protocol.to_uppercase()))?; key.0.set_value("URL Protocol", &"")?; let command_key = key.0.create_subkey(r"shell\open\command")?; @@ -122,12 +123,10 @@ pub fn unregister_url_protocol(protocol: &str, identifier: Option<&str>, mode: & }; let delete = match key.open_subkey(protocol) { - Ok(k) => { - match k.get_value::("_menuinst") { - Ok(value) => identifier.is_none() || Some(value.as_str()) == identifier, - Err(_) => identifier.is_none(), - } - } + Ok(k) => match k.get_value::("_menuinst") { + Ok(value) => identifier.is_none() || Some(value.as_str()) == identifier, + Err(_) => identifier.is_none(), + }, Err(e) => { tracing::error!("Could not check key {} for deletion: {}", protocol, e); return Ok(()); diff --git a/test-data/menuinst/spyder/menu.json b/test-data/menuinst/spyder/menu.json new file mode 100644 index 000000000..1ea556661 --- /dev/null +++ b/test-data/menuinst/spyder/menu.json @@ -0,0 +1,161 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "$id": "https://schemas.conda.io/menuinst-1.schema.json", + "menu_name": "superspyder", + "menu_items": [ + { + "name": { + "target_environment_is_base": "superspyder 1.2 (base)", + "target_environment_is_not_base": "superspyder 1.2 ({{ ENV_NAME }})" + }, + "description": "Scientific PYthon Development EnviRonment", + "icon": "{{ MENU_DIR }}/superspyder.{{ ICON_EXT }}", + "activate": false, + "terminal": false, + "command": [""], + "platforms": { + "win": { + "desktop": true, + "app_user_model_id": "superspyder-ide.superspyder-__PKG_MAJOR_VER__.{{ ENV_NAME }}", + "command": ["{{ PREFIX }}/Scripts/superspyder.exe", "%*"], + "file_extensions": [ + ".enaml", + ".ipy", + ".py", + ".pyi", + ".pyw", + ".pyx" + ] + }, + "linux": { + "Categories": [ + "Development", + "Science" + ], + "command": ["{{ PREFIX }}/bin/spyder", "%F"], + "StartupWMClass": "Spyder-__PKG_MAJOR_VER__.{{ ENV_NAME }}", + "MimeType": [ + "text/x-python" + ] + }, + "osx": { + "precommand": "pushd \"$(dirname \"$0\")\" &>/dev/null", + "command": ["./python", "{{ PREFIX }}/bin/spyder", "$@"], + "link_in_bundle": { + "{{ PREFIX }}/bin/python": "{{ MENU_ITEM_LOCATION }}/Contents/MacOS/python" + }, + "CFBundleName": "Superspyder __PKG_MAJOR_VER__", + "CFBundleIdentifier": "org.superspyder-ide.Superspyder-__PKG_MAJOR_VER__-__CFBID_ENV__", + "CFBundleVersion": "__PKG_VERSION__", + "CFBundleDocumentTypes": [ + { + "CFBundleTypeName": "text document", + "CFBundleTypeRole": "Editor", + "LSHandlerRank": "Default", + "CFBundleTypeIconFile": "superspyder.icns", + "LSItemContentTypes": [ + "com.apple.applescript.text", + "com.apple.ascii-property-list", + "com.apple.audio-unit-preset", + "com.apple.binary-property-list", + "com.apple.configprofile", + "com.apple.crashreport", + "com.apple.dashcode.css", + "com.apple.dashcode.javascript", + "com.apple.dashcode.json", + "com.apple.dashcode.manifest", + "com.apple.dt.document.ascii-property-list", + "com.apple.dt.document.script-suite-property-list", + "com.apple.dt.document.script-terminology-property-list", + "com.apple.property-list", + "com.apple.rez-source", + "com.apple.scripting-definition", + "com.apple.structured-text", + "com.apple.traditional-mac-plain-text", + "com.apple.xcode.ada-source", + "com.apple.xcode.apinotes", + "com.apple.xcode.bash-script", + "com.apple.xcode.configsettings", + "com.apple.xcode.csh-script", + "com.apple.xcode.entitlements-property-list", + "com.apple.xcode.fortran-source", + "com.apple.xcode.glsl-source", + "com.apple.xcode.ksh-script", + "com.apple.xcode.lex-source", + "com.apple.xcode.make-script", + "com.apple.xcode.mig-source", + "com.apple.xcode.pascal-source", + "com.apple.xcode.strings-text", + "com.apple.xcode.tcsh-script", + "com.apple.xcode.yacc-source", + "com.apple.xcode.zsh-script", + "com.apple.xml-property-list", + "com.netscape.javascript-source", + "com.scenarist.closed-caption", + "com.sun.java-source", + "com.sun.java-web-start", + "net.daringfireball.markdown", + "org.khronos.glsl-source", + "org.oasis-open.xliff", + "public.ada-source", + "public.assembly-source", + "public.bash-script", + "public.c-header", + "public.c-plus-plus-header", + "public.c-plus-plus-source", + "public.c-source", + "public.case-insensitive-text", + "public.comma-separated-values-text", + "public.csh-script", + "public.css", + "public.delimited-values-text", + "public.dylan-source", + "public.filename-extension", + "public.fortran-77-source", + "public.fortran-90-source", + "public.fortran-95-source", + "public.fortran-source", + "public.html", + "public.json", + "public.ksh-script", + "public.lex-source", + "public.log", + "public.m3u-playlist", + "public.make-source", + "public.mig-source", + "public.mime-type", + "public.module-map", + "public.nasm-assembly-source", + "public.objective-c-plus-plus-source", + "public.objective-c-source", + "public.opencl-source", + "public.pascal-source", + "public.patch-file", + "public.perl-script", + "public.php-script", + "public.plain-text", + "public.python-script", + "public.rss", + "public.ruby-script", + "public.script", + "public.shell-script", + "public.source-code", + "public.tcsh-script", + "public.text", + "public.utf16-external-plain-text", + "public.utf16-plain-text", + "public.utf8-plain-text", + "public.utf8-tab-separated-values-text", + "public.xhtml", + "public.xml", + "public.yacc-source", + "public.yaml", + "public.zsh-script" + ] + } + ] + } + } + } + ] +} \ No newline at end of file