Skip to content

Commit

Permalink
feat: add rattler_menuinst crate (#840)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Bas Zalmstra <bas@prefix.dev>
Co-authored-by: Julian Hofer <julianhofer@gnome.org>
Co-authored-by: Ruben Arts <ruben.arts@hotmail.com>
  • Loading branch information
4 people authored Feb 25, 2025
1 parent f81bed6 commit 02d0292
Show file tree
Hide file tree
Showing 55 changed files with 8,571 additions and 3 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,10 @@ tracing-test = { version = "0.2.5" }
trybuild = { version = "1.0.103" }
typed-path = { version = "0.10.0" }
url = { version = "2.5.4" }
unicode-normalization = "0.1.24"
uuid = { version = "1.13.1", default-features = false }
walkdir = "2.5.0"
which = "7.0.2"
windows-sys = { version = "0.59.0", default-features = false }
winver = { version = "1.0.0" }
zip = { version = "2.2.2", default-features = false }
Expand Down
1 change: 1 addition & 0 deletions crates/rattler-bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ rattler_repodata_gateway = { path="../rattler_repodata_gateway", version = "0.21
rattler_solve = { path="../rattler_solve", version = "1.3.8", default-features = false, features = ["resolvo", "libsolv_c"] }
rattler_virtual_packages = { path="../rattler_virtual_packages", version = "2.0.3", default-features = false }
rattler_cache = { path="../rattler_cache", version = "0.3.9", default-features = false }
rattler_menuinst = { path="../rattler_menuinst", version = "0.1.0", default-features = false }
reqwest = { workspace = true }
reqwest-middleware = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
Expand Down
59 changes: 59 additions & 0 deletions crates/rattler-bin/src/commands/menu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use anyhow::{Context, Result};
use clap::Parser;
use rattler_conda_types::{menuinst::MenuMode, PackageName, Platform, PrefixRecord};
use std::{fs, path::PathBuf};

#[derive(Debug, Parser)]
pub struct InstallOpt {
/// Target prefix to look for the package (defaults to `.prefix`)
#[clap(long, short, default_value = ".prefix")]
target_prefix: PathBuf,

/// Name of the package for which to install menu items
package_name: PackageName,
}

pub async fn install_menu(opts: InstallOpt) -> Result<()> {
// Find the prefix record in the target_prefix and call `install_menu` on it
let records: Vec<PrefixRecord> = PrefixRecord::collect_from_prefix(&opts.target_prefix)?;

let record = records
.iter()
.find(|r| r.repodata_record.package_record.name == opts.package_name)
.with_context(|| {
format!(
"Package {} not found in prefix {:?}",
opts.package_name.as_normalized(),
opts.target_prefix
)
})?;
let prefix = fs::canonicalize(&opts.target_prefix)?;
rattler_menuinst::install_menuitems_for_record(
&prefix,
record,
Platform::current(),
MenuMode::User,
)?;

Ok(())
}

pub async fn remove_menu(opts: InstallOpt) -> Result<()> {
// Find the prefix record in the target_prefix and call `remove_menu` on it
let records: Vec<PrefixRecord> = PrefixRecord::collect_from_prefix(&opts.target_prefix)?;

let record = records
.iter()
.find(|r| r.repodata_record.package_record.name == opts.package_name)
.with_context(|| {
format!(
"Package {} not found in prefix {:?}",
opts.package_name.as_normalized(),
opts.target_prefix
)
})?;

rattler_menuinst::remove_menu_items(&record.installed_system_menus)?;

Ok(())
}
1 change: 1 addition & 0 deletions crates/rattler-bin/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod create;
pub mod menu;
pub mod virtual_packages;
4 changes: 4 additions & 0 deletions crates/rattler-bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ struct Opt {
enum Command {
Create(commands::create::Opt),
VirtualPackages(commands::virtual_packages::Opt),
InstallMenu(commands::menu::InstallOpt),
RemoveMenu(commands::menu::InstallOpt),
}

/// Entry point of the `rattler` cli.
Expand Down Expand Up @@ -72,5 +74,7 @@ async fn main() -> anyhow::Result<()> {
match opt.command {
Command::Create(opts) => commands::create::create(opts).await,
Command::VirtualPackages(opts) => commands::virtual_packages::virtual_packages(opts),
Command::InstallMenu(opts) => commands::menu::install_menu(opts).await,
Command::RemoveMenu(opts) => commands::menu::remove_menu(opts).await,
}
}
2 changes: 2 additions & 0 deletions crates/rattler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ rattler_digest = { path = "../rattler_digest", version = "1.0.6", default-featur
rattler_networking = { path = "../rattler_networking", version = "0.22.4", default-features = false }
rattler_shell = { path = "../rattler_shell", version = "0.22.19", default-features = false }
rattler_package_streaming = { path = "../rattler_package_streaming", version = "0.22.28", default-features = false, features = ["reqwest"] }
rattler_menuinst = { path = "../rattler_menuinst", version = "0.1.0", default-features = false }
rayon = { workspace = true }
reflink-copy = { workspace = true }
regex = { workspace = true }
Expand All @@ -52,6 +53,7 @@ tracing = { workspace = true }
url = { workspace = true, features = ["serde"] }
uuid = { workspace = true, features = ["v4", "fast-rng"] }
console = { workspace = true, optional = true }
serde_json.workspace = true

[dev-dependencies]
assert_matches = { workspace = true }
Expand Down
18 changes: 16 additions & 2 deletions crates/rattler/src/install/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,18 +155,32 @@ impl InstallDriver {
transaction: &Transaction<Old, New>,
target_prefix: &Path,
) -> Result<Option<PrePostLinkResult>, PrePostLinkError> {
let mut result = None;
if self.execute_link_scripts {
match self.run_pre_unlink_scripts(transaction, target_prefix) {
Ok(res) => {
return Ok(Some(res));
result = Some(res);
}
Err(e) => {
tracing::error!("Error running pre-unlink scripts: {:?}", e);
}
}
}

Ok(None)
// For all packages that are removed, we need to remove menuinst entries as well
for record in transaction.removed_packages() {
let prefix_record = record.borrow();
if !prefix_record.installed_system_menus.is_empty() {
match rattler_menuinst::remove_menu_items(&prefix_record.installed_system_menus) {
Ok(_) => {}
Err(e) => {
tracing::warn!("Failed to remove menu item: {}", e);
}
}
}
}

Ok(result)
}

/// Runs a blocking task that will execute on a separate thread. The task is
Expand Down
1 change: 1 addition & 0 deletions crates/rattler/src/install/installer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ async fn link_package(
// ...
link_type: Some(LinkType::HardLink),
}),
installed_system_menus: Vec::new(),
};

let conda_meta_path = target_prefix.join("conda-meta");
Expand Down
1 change: 1 addition & 0 deletions crates/rattler/src/install/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub async fn install_package_to_environment(
paths_data: paths.into(),
requested_spec: None,
link: None,
installed_system_menus: Vec::new(),
};

// Create the conda-meta directory if it doesnt exist yet.
Expand Down
1 change: 1 addition & 0 deletions crates/rattler_conda_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod channel;
mod channel_data;
mod explicit_environment_spec;
mod match_spec;
pub mod menuinst;
mod no_arch_type;
mod parse_mode;
mod platform;
Expand Down
132 changes: 132 additions & 0 deletions crates/rattler_conda_types/src/menuinst/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//! Define types that can be serialized into a `PrefixRecord` to track
//! menu entries installed into the system.
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Menu mode that was used to install the menu entries
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum MenuMode {
/// System-wide installation
System,

/// User installation
#[default]
User,
}

/// Tracker for menu entries installed into the system
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Tracker {
/// Linux tracker
Linux(LinuxTracker),
/// Windows tracker
Windows(WindowsTracker),
/// macOS tracker
MacOs(MacOsTracker),
}

/// Registered MIME file on the system
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LinuxRegisteredMimeFile {
/// The application that was registered
pub application: String,
/// Path to use when calling `update-mime-database`
pub database_path: PathBuf,
/// The location of the config file that was edited
pub config_file: PathBuf,
/// The MIME types that were associated to the application
pub mime_types: Vec<String>,
}

/// Tracker for Linux installations
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LinuxTracker {
/// The menu mode that was used to install the menu entries
pub install_mode: MenuMode,

/// List of desktop files that were installed
pub paths: Vec<PathBuf>,

/// MIME types that were installed
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mime_types: Option<LinuxRegisteredMimeFile>,

/// MIME type glob files that were registered on the system
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub registered_mime_files: Vec<PathBuf>,
}

/// File extension that was installed
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WindowsFileExtension {
/// The file extension that was installed
pub extension: String,
/// The identifier of the file extension
pub identifier: String,
}

/// URL protocol that was installed
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WindowsUrlProtocol {
/// The URL protocol that was installed
pub protocol: String,
/// The identifier of the URL protocol
pub identifier: String,
}

/// Terminal profile that was installed
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WindowsTerminalProfile {
/// The name of the terminal profile
pub configuration_file: PathBuf,
/// The identifier of the terminal profile
pub identifier: String,
}

/// Tracker for Windows installations
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WindowsTracker {
/// The menu mode that was used to install the menu entries
pub menu_mode: MenuMode,

/// List of shortcuts that were installed
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shortcuts: Vec<PathBuf>,

/// List of file extensions that were installed
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub file_extensions: Vec<WindowsFileExtension>,

/// List of URL protocols that were installed
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub url_protocols: Vec<WindowsUrlProtocol>,

/// List of terminal profiles that were installed
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub terminal_profiles: Vec<WindowsTerminalProfile>,
}

impl WindowsTracker {
/// Create a new Windows tracker
pub fn new(menu_mode: MenuMode) -> Self {
Self {
menu_mode,
shortcuts: Vec::new(),
file_extensions: Vec::new(),
url_protocols: Vec::new(),
terminal_profiles: Vec::new(),
}
}
}

/// Tracker for macOS installations
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MacOsTracker {
/// The app folder that was installed, e.g. ~/Applications/foobar.app
pub app_folder: PathBuf,
/// Argument that was used to call `lsregister` and that we need to
/// call to unregister the app
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lsregister: Option<PathBuf>,
}
8 changes: 7 additions & 1 deletion crates/rattler_conda_types/src/prefix_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use crate::package::FileMode;
use crate::repo_data::RecordFromPath;
use crate::repo_data_record::RepoDataRecord;
use crate::PackageRecord;
use crate::{menuinst, PackageRecord};
use rattler_digest::serde::SerializableHash;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
Expand Down Expand Up @@ -179,6 +179,11 @@ pub struct PrefixRecord {
/// currently another spec was used. Note: conda seems to serialize a "None" string value instead of `null`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requested_spec: Option<String>,

/// If menuinst is enabled and added menu items, this field contains the menuinst tracker data.
/// This data is used to remove the menu items when the package is uninstalled.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub installed_system_menus: Vec<menuinst::Tracker>,
}

impl PrefixRecord {
Expand Down Expand Up @@ -209,6 +214,7 @@ impl PrefixRecord {
paths_data: paths.into(),
link,
requested_spec,
installed_system_menus: Vec::new(),
}
}

Expand Down
50 changes: 50 additions & 0 deletions crates/rattler_menuinst/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[package]
name = "rattler_menuinst"
version = "0.1.0"
edition.workspace = true
authors = ["Wolf Vollprecht <w.vollprecht@gmail.com>"]
description = "Install menu entries for a Conda package"
categories.workspace = true
homepage.workspace = true
repository.workspace = true
license.workspace = true
readme.workspace = true

[dependencies]
dirs = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tracing = { workspace = true }
rattler_conda_types = { path = "../rattler_conda_types", default-features = false }
rattler_shell = { path = "../rattler_shell", default-features = false }
thiserror = { workspace = true }
unicode-normalization = { workspace = true }
regex = { workspace = true }
tempfile = { workspace = true }
fs-err = { workspace = true }
which = { workspace = true }
chrono = { workspace = true, features = ["clock"] }
once_cell = {workspace = true}

[target.'cfg(target_os = "macos")'.dependencies]
plist = { workspace = true }
sha2 = { workspace = true }

[target.'cfg(target_os = "linux")'.dependencies]
quick-xml = "0.37.2"
configparser = { version = "3.1.0" }
shlex = { workspace = true }

[target.'cfg(target_os = "windows")'.dependencies]
known-folders = "1.2.0"
windows = { version = "0.60.0", features = [
"Win32_System_Com_StructuredStorage",
"Win32_UI_Shell_PropertiesSystem",
"Win32_Storage_EnhancedStorage",
"Win32_System_Variant",
]}
windows-registry = "0.5.0"

[dev-dependencies]
insta = { workspace = true, features = ["json"] }
configparser = { version = "3.1.0", features = ["indexmap"] }
Binary file not shown.
Binary file not shown.
Binary file added crates/rattler_menuinst/data/osx_launcher_arm64
Binary file not shown.
Binary file added crates/rattler_menuinst/data/osx_launcher_x86_64
Binary file not shown.
Loading

0 comments on commit 02d0292

Please sign in to comment.