diff --git a/Cargo.lock b/Cargo.lock index a4cdad6..25078f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -774,6 +774,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "junction" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca39ef0d69b18e6a2fd14c2f0a1d593200f4a4ed949b240b5917ab51fac754cb" +dependencies = [ + "scopeguard", + "winapi", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -810,6 +820,7 @@ dependencies = [ "flume", "futures", "git2", + "junction", "log", "once_cell", "rayon", diff --git a/crates/libscoop/Cargo.toml b/crates/libscoop/Cargo.toml index 322285a..07f7c2f 100644 --- a/crates/libscoop/Cargo.toml +++ b/crates/libscoop/Cargo.toml @@ -18,6 +18,7 @@ dirs = "5.0.1" flume = "0.10" futures = { version = "0.3", features = ["thread-pool"] } git2 = "0.17.2" +junction = "1.0.0" log = "0.4" once_cell = "1.18.0" rayon = "1.7.0" diff --git a/crates/libscoop/src/env.rs b/crates/libscoop/src/env.rs new file mode 100644 index 0000000..fcec4bf --- /dev/null +++ b/crates/libscoop/src/env.rs @@ -0,0 +1,60 @@ +use crate::{error::Fallible, internal, package::Package, Event, Session}; + +/// Unset all environment variables defined by a given package. +pub fn remove(session: &Session, package: &Package) -> Fallible<()> { + assert!(package.is_installed()); + + // Unset environment variables + if let Some(env_set) = package.manifest().env_set() { + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackageEnvVarRemoveStart); + } + + let keys = env_set.keys(); + for key in keys { + internal::env::set(key, "")?; + } + + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackageEnvVarRemoveDone); + } + } + + // Remove environment path + if let Some(env_add_path) = package.manifest().env_add_path() { + let mut env_path_list = internal::env::get_env_path_list()?; + let config = session.config(); + let mut app_path = config.root_path().join("apps"); + app_path.push(package.name()); + + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackageEnvPathRemoveStart); + } + + let version = if config.no_junction() { + package.installed_version().unwrap() + } else { + "current" + }; + + let paths = env_add_path + .into_iter() + .map(|p| { + internal::path::normalize_path(app_path.join(version).join(p)) + .to_str() + .unwrap() + .to_owned() + }) + .collect::>(); + + env_path_list.retain(|p| !paths.contains(p)); + + internal::env::set("PATH", &env_path_list.join(";"))?; + + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackageEnvPathRemoveDone); + } + } + + Ok(()) +} diff --git a/crates/libscoop/src/event.rs b/crates/libscoop/src/event.rs index 17ae070..f81d335 100644 --- a/crates/libscoop/src/event.rs +++ b/crates/libscoop/src/event.rs @@ -69,43 +69,88 @@ pub enum Event { /// Bucket update has made some progress. BucketUpdateProgress(BucketUpdateProgressContext), - /// Bucket update is finished. + /// Bucket update has finished. BucketUpdateDone, - /// Package resolving is started. - PackageResolveStart, + /// Package has started to be committed. + PackageCommitStart(String), - /// Package resolving is finished. - PackageResolveDone, + /// Package has been committed. + PackageCommitDone(String), - /// Calculating download size is started. + /// Calculating download size has started. PackageDownloadSizingStart, - /// Calculating download size is finished. + /// Calculating download size has finished. PackageDownloadSizingDone, - /// Package download is started. + /// Package download has started. PackageDownloadStart, /// Package download has made some progress. PackageDownloadProgress(PackageDownloadProgressContext), - /// Package download is finished. + /// Package download has finished. PackageDownloadDone, - /// Package integrity check is started. + /// Package environment path(s) removal has started. + PackageEnvPathRemoveStart, + + /// Package environment path(s) removal has finished. + PackageEnvPathRemoveDone, + + /// Package environment variable(s) removal has started. + PackageEnvVarRemoveStart, + + /// Package environment variable(s) removal has finished. + PackageEnvVarRemoveDone, + + /// Package integrity check has started. PackageIntegrityCheckStart, /// Package integrity check has made some progress. PackageIntegrityCheckProgress(String), - /// Package integrity check is finished. + /// Package integrity check has finished. PackageIntegrityCheckDone, - /// Package is started to be committed. - PackageCommitStart(String), + /// Package persist removal has started. + PackagePersistPurgeStart, + + /// Package persist removal has finished. + PackagePersistPurgeDone, + + /// Package PowerShell module removal has started. + PackagePsModuleRemoveStart(String), + + /// Package PowerShell module removal has finished. + PackagePsModuleRemoveDone, + + /// Package resolving has started. + PackageResolveStart, + + /// Package resolving has finished. + PackageResolveDone, + + /// Package shim removal has started. + PackageShimRemoveStart, + + /// Package shim removal has made some progress. + PackageShimRemoveProgress(String), + + /// Package shim removal has finished. + PackageShimRemoveDone, + + /// Package shortcut removal has started. + PackageShortcutRemoveStart, + + /// Package shortcut removal has made some progress. + PackageShortcutRemoveProgress(String), + + /// Package shortcut removal has finished. + PackageShortcutRemoveDone, - /// Package sync operation is finished. + /// Package sync operation has finished. PackageSyncDone, /// Prompt the user to confirm the transaction. diff --git a/crates/libscoop/src/internal/env.rs b/crates/libscoop/src/internal/env.rs index 85cece1..664363e 100644 --- a/crates/libscoop/src/internal/env.rs +++ b/crates/libscoop/src/internal/env.rs @@ -24,9 +24,21 @@ pub fn set(key: &str, value: &str) -> Fallible<()> { let (env, _) = HKCU.create_subkey(path)?; if value.is_empty() { - env.delete_value(key)?; + // ignore error of deleting non-existent value + let _ = env.delete_value(key); } else { env.set_value(key, &value)?; } Ok(()) } + +/// Get the value of the `PATH` environment variable as a list of paths. +pub fn get_env_path_list() -> Fallible> { + let env_path = get("PATH")?; + Ok(env_path + .into_string() + .unwrap() + .split(';') + .map(|s| s.to_owned()) + .collect()) +} diff --git a/crates/libscoop/src/internal/fs.rs b/crates/libscoop/src/internal/fs.rs index 9a1e1b3..6460267 100644 --- a/crates/libscoop/src/internal/fs.rs +++ b/crates/libscoop/src/internal/fs.rs @@ -17,8 +17,9 @@ pub fn ensure_dir + ?Sized>(path: &P) -> io::Result<()> { std::fs::create_dir_all(path.as_ref()) } -pub fn remove_dir + ?Sized>(path: &P) -> io::Result<()> { - remove_dir_all::remove_dir_all(path.as_ref()) +/// Remove given `path` recursively. +pub fn remove_dir>(path: P) -> io::Result<()> { + remove_dir_all::remove_dir_all(path) } /// Remove all files and subdirectories in given `path`. @@ -83,3 +84,52 @@ where .open(path)?; Ok(serde_json::to_writer_pretty(file, &data)?) } + +/// Remove a symlink at `lnk`. +pub fn remove_symlink>(lnk: P) -> io::Result<()> { + let lnk = lnk.as_ref(); + let metadata = lnk.symlink_metadata()?; + let mut permissions = metadata.permissions(); + + // Remove possible readonly flag on the symlink added by `attrib +R` command + if permissions.readonly() { + // Remove readonly flag + permissions.set_readonly(false); + std::fs::set_permissions(lnk, permissions)?; + } + + // We knew that `lnk` is a symlink but we don't know if it is a file or a + // directory. So we need to check its metadata to determine how to remove + // it. The file type of the symlink itself is always `FileType::Symlink` + // and `symlink_metadata::is_dir` always returns `false` for symlinks, so + // we have to check the metadata of the target file. + let target_metadata = lnk.metadata()?; + + if target_metadata.is_dir() { + std::fs::remove_dir(lnk) + } else { + std::fs::remove_file(lnk) + } +} + +/// Create a directory symlink at `lnk` pointing to `src`. +pub fn symlink_dir, Q: AsRef>(src: P, lnk: Q) -> io::Result<()> { + // It is possible to create a symlink on Windows, but one of the following + // conditions must be met: + // + // Either: the process has the `SeCreateSymbolicLinkPrivilege` privilege, + // or: the OS is Windows 10 Creators Update or later and Developer Mode + // enabled. + // + // We prefer symlink over junction because: + // https://stackoverflow.com/questions/9042542/what-is-the-difference-between-ntfs-junction-points-and-symbolic-links + // + // Here we try to create a symlink first, and if it fails, we try to create + // a junction which does not require any special privilege and works on + // older versions of Windows. + if std::os::windows::fs::symlink_dir(src.as_ref(), lnk.as_ref()).is_err() { + junction::create(src, lnk) + } else { + Ok(()) + } +} diff --git a/crates/libscoop/src/lib.rs b/crates/libscoop/src/lib.rs index 2d49c95..5afbf14 100644 --- a/crates/libscoop/src/lib.rs +++ b/crates/libscoop/src/lib.rs @@ -37,11 +37,16 @@ mod bucket; mod cache; mod config; mod constant; +mod env; mod error; mod event; mod internal; mod package; +mod persist; +mod psmodule; mod session; +mod shim; +mod shortcut; pub mod operation; diff --git a/crates/libscoop/src/package/manifest.rs b/crates/libscoop/src/package/manifest.rs index 65f0e0e..98fe664 100644 --- a/crates/libscoop/src/package/manifest.rs +++ b/crates/libscoop/src/package/manifest.rs @@ -95,6 +95,8 @@ pub struct ManifestSpec { #[serde(skip_serializing_if = "Option::is_none")] pub bin: Option>>, + /// The `env_add_path` field is used to define path(s) that need to be added + /// to the `PATH` environment variable during installation. #[serde(skip_serializing_if = "Option::is_none")] pub env_add_path: Option>, @@ -108,11 +110,15 @@ pub struct ManifestSpec { #[serde(skip_serializing_if = "Option::is_none")] pub shortcuts: Option>>, + /// The `persist` field is used to define files/directories that need to be + /// persisted during uninstallation. #[serde(skip_serializing_if = "Option::is_none")] - pub persist: Option>>, + persist: Option>>, + /// The `psmodule` field is used to define PowerShell module that need to + /// be imported during installation. #[serde(skip_serializing_if = "Option::is_none")] - pub psmodule: Option, + psmodule: Option, #[serde(skip_serializing_if = "Option::is_none")] pub suggest: Option>>, @@ -160,12 +166,17 @@ pub struct Vectorized(Vec); #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Architecture { + /// Ia32 architecture specification. #[serde(rename = "32bit")] #[serde(skip_serializing_if = "Option::is_none")] pub ia32: Option, + + /// Amd64 architecture specification. #[serde(rename = "64bit")] #[serde(skip_serializing_if = "Option::is_none")] pub amd64: Option, + + /// Aarch64 architecture specification. #[serde(rename = "arm64")] #[serde(skip_serializing_if = "Option::is_none")] pub aarch64: Option, @@ -193,15 +204,17 @@ pub struct Uninstaller { pub script: Option>, } +/// PowerShell module information of a Scoop package. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Psmodule { - pub name: String, + name: String, } #[derive(Clone, Debug, Serialize)] pub struct Sourceforge { #[serde(skip_serializing_if = "Option::is_none")] pub project: Option, + pub path: String, } @@ -210,21 +223,29 @@ pub struct Checkver { #[serde(alias = "re")] #[serde(skip_serializing_if = "Option::is_none")] pub regex: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, + #[serde(alias = "jp")] #[serde(skip_serializing_if = "Option::is_none")] pub jsonpath: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub xpath: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub reverse: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub replace: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub useragent: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub script: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub sourceforge: Option, } @@ -233,12 +254,16 @@ pub struct Checkver { pub struct Autoupdate { #[serde(skip_serializing_if = "Option::is_none")] pub architecture: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub extract_dir: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub hash: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub notes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub url: Option>, } @@ -252,6 +277,7 @@ pub struct ArchitectureSpec { #[serde(skip_serializing_if = "Option::is_none")] pub checkver: Option, + /// Same as `ManifestSpec::env_add_path` #[serde(skip_serializing_if = "Option::is_none")] pub env_add_path: Option>, @@ -806,6 +832,13 @@ impl Manifest { self.inner.cookie.as_ref() } + /// Returns `env_add_path` defined in this manifest. + #[inline] + pub fn env_add_path(&self) -> Option> { + let ret = arch_specific_field!(self, env_add_path); + ret.map(|v| v.devectorize()) + } + /// Returns `env_set` defined in this manifest. #[inline] pub fn env_set(&self) -> Option<&HashMap> { @@ -867,6 +900,18 @@ impl Manifest { arch_specific_field!(self, uninstaller) } + /// Returns `persist` defined in this manifest. + #[inline] + pub fn persist(&self) -> Option>> { + self.inner.persist.as_ref().map(|v| v.devectorize()) + } + + /// Returns `psmodule` defined in this manifest. + #[inline] + pub fn psmodule(&self) -> Option<&Psmodule> { + self.inner.psmodule.as_ref() + } + #[inline] pub fn shortcuts(&self) -> Option>> { let ret = arch_specific_field!(self, shortcuts); @@ -1051,6 +1096,13 @@ impl Installer { } } +impl Psmodule { + /// Return the `name` of the PowerShell module. + pub fn name(&self) -> &str { + &self.name + } +} + impl Uninstaller { #[inline] pub fn args(&self) -> Option> { diff --git a/crates/libscoop/src/package/mod.rs b/crates/libscoop/src/package/mod.rs index c11b23a..3dff877 100644 --- a/crates/libscoop/src/package/mod.rs +++ b/crates/libscoop/src/package/mod.rs @@ -358,8 +358,9 @@ impl Package { ret } - /// Check if this package has used powershell script hooks in its manifest. - pub(crate) fn has_ps_script(&self) -> bool { + /// Check if this package defines install hooks (powershell scripts) in its + /// manifest. + pub(crate) fn has_install_script(&self) -> bool { [ self.manifest.pre_install(), self.manifest.post_install(), @@ -367,6 +368,15 @@ impl Package { .installer() .map(|i| i.script()) .unwrap_or_default(), + ] + .into_iter() + .any(|h| h.is_some()) + } + + /// Check if this package defines uninstall hooks (powershell scripts) in its + /// manifest. + pub(crate) fn has_uninstall_script(&self) -> bool { + [ self.manifest .uninstaller() .map(|u| u.script()) diff --git a/crates/libscoop/src/package/sync.rs b/crates/libscoop/src/package/sync.rs index b1cee84..8801888 100644 --- a/crates/libscoop/src/package/sync.rs +++ b/crates/libscoop/src/package/sync.rs @@ -3,7 +3,10 @@ use once_cell::unsync::OnceCell; use scoop_hash::ChecksumBuilder; use std::io::Read; -use crate::{error::Fallible, Error, Event, QueryOption, Session}; +use crate::{ + env, error::Fallible, internal, persist, psmodule, shim, shortcut, Error, Event, QueryOption, + Session, +}; use super::{ download::{self, DownloadSize}, @@ -535,6 +538,12 @@ pub fn install(session: &Session, queries: &[&str], options: &[SyncOption]) -> F let download_only = options.contains(&SyncOption::DownloadOnly); if !download_only { + // TODO: PowerShell hosting with execution context is not supported yet. + // Perhaps at present we could call Scoop to do the removal for packages + // using PS scripts... + let (_packages_with_script, _packages): (Vec<&Package>, Vec<&Package>) = + packages.iter().partition(|p| p.has_install_script()); + // TODO: commit transcation // let config = session.config(); // let apps_dir = config.root_path().join("apps"); @@ -648,8 +657,15 @@ pub fn remove(session: &Session, queries: &[&str], options: &[SyncOption]) -> Fa // TODO: PowerShell hosting with execution context is not supported yet. // Perhaps at present we could call Scoop to do the removal for packages // using PS scripts... - let (_packages_with_script, _packages): (Vec<_>, Vec<_>) = - packages.iter().partition(|p| p.has_ps_script()); + let (packages_with_script, _packages): (Vec<_>, Vec<_>) = + packages.iter().partition(|p| p.has_uninstall_script()); + + // TODO: support removal of packages with PowerShell script + if !packages_with_script.is_empty() { + let msg = format!("Found package(s) using PowerShell script:\n {}\nRemoval of package with PowerShell script is not yet supported.", + packages_with_script.iter().map(|p| p.name()).collect::>().join(" ")); + return Err(Error::Custom(msg)); + } transaction.set_remove(packages); @@ -657,7 +673,7 @@ pub fn remove(session: &Session, queries: &[&str], options: &[SyncOption]) -> Fa if !assume_yes { if let Some(tx) = session.emitter() { if tx - .send(Event::PromptTransactionNeedConfirm(transaction)) + .send(Event::PromptTransactionNeedConfirm(transaction.clone())) .is_ok() { let rx = session.receiver().unwrap(); @@ -677,8 +693,53 @@ pub fn remove(session: &Session, queries: &[&str], options: &[SyncOption]) -> Fa } } - // TODO: commit transcation - // let purge = options.contains(&SyncOption::Purge); + if let Some(packages) = transaction.remove_view() { + let purge = options.contains(&SyncOption::Purge); + let config = session.config(); + let root_dir = config.root_path(); + + for package in packages.iter() { + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackageCommitStart(package.name().to_owned())); + } + + let app_dir = root_dir.join("apps").join(package.name()); + + // TODO: pre_uninstall + // TODO: uninstaller + + shim::remove(session, package)?; + shortcut::remove(session, package)?; + psmodule::remove(session, package)?; + env::remove(session, package)?; + persist::unlink(session, package)?; + + let current_lnk = app_dir.join("current"); + internal::fs::remove_symlink(current_lnk)?; + + // TODO: post_uninstall + + // Remove the app directory + internal::fs::remove_dir(app_dir)?; + + if purge { + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackagePersistPurgeStart); + } + + let persist_dir = config.root_path().join("persist").join(package.name()); + internal::fs::remove_dir(persist_dir)?; + + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackagePersistPurgeDone); + } + } + + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackageCommitDone(package.name().to_owned())); + } + } + } Ok(()) } diff --git a/crates/libscoop/src/persist.rs b/crates/libscoop/src/persist.rs new file mode 100644 index 0000000..d353f60 --- /dev/null +++ b/crates/libscoop/src/persist.rs @@ -0,0 +1,27 @@ +use crate::{error::Fallible, internal, package::Package, Session}; + +/// Remove PowerShell module imported by a given package. +pub fn unlink(session: &Session, package: &Package) -> Fallible<()> { + assert!(package.is_installed()); + + if let Some(persists) = package.manifest().persist() { + let config = session.config(); + let mut app_path = config.root_path().join("apps"); + app_path.push(package.name()); + + let version = if config.no_junction() { + package.installed_version().unwrap() + } else { + "current" + }; + + let persist_path = app_path.join(version); + for persist in persists { + assert!(!persist.is_empty()); + + let src = internal::path::normalize_path(persist_path.join(persist[0])); + internal::fs::remove_symlink(src)?; + } + } + Ok(()) +} diff --git a/crates/libscoop/src/psmodule.rs b/crates/libscoop/src/psmodule.rs new file mode 100644 index 0000000..cb789d2 --- /dev/null +++ b/crates/libscoop/src/psmodule.rs @@ -0,0 +1,25 @@ +use crate::{error::Fallible, package::Package, Event, Session}; + +/// Remove PowerShell module imported by a given package. +pub fn remove(session: &Session, package: &Package) -> Fallible<()> { + assert!(package.is_installed()); + + if let Some(psmodule) = package.manifest().psmodule() { + let config = session.config(); + let mut psmodule_path = config.root_path().join("modules"); + + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackagePsModuleRemoveStart( + psmodule.name().to_owned(), + )); + } + + psmodule_path.push(psmodule.name()); + let _ = std::fs::remove_dir(psmodule_path); + + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackagePsModuleRemoveDone); + } + } + Ok(()) +} diff --git a/crates/libscoop/src/shim.rs b/crates/libscoop/src/shim.rs new file mode 100644 index 0000000..81e2be6 --- /dev/null +++ b/crates/libscoop/src/shim.rs @@ -0,0 +1,199 @@ +#![allow(dead_code)] +use std::path::Path; + +use crate::{error::Fallible, internal, package::Package, Event, Session}; + +#[derive(Debug)] +pub struct Shim<'a> { + name: &'a str, + real_name: &'a str, + ty: ShimType, + args: Option>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ShimType { + /// Bash script + /// + /// A shim will be treated as a Bash script if it does not have a file + /// extension. + Bash, + + /// Batch script + /// + /// A shim will be treated as a Batch script if it has a `.bat`/`.cmd` file + /// extension. + Batch, + + /// Executable + /// + /// A shim will be treated as an executable if it has a `.exe`/`.com` file + /// extension. + Exe, + + /// Java JAR + /// + /// A shim will be treated as a Java JAR if it has a `.jar` file extension. + Java, + + /// PowerShell script + /// + /// A shim will be treated as a PowerShell script if it has a `.ps1` file + /// extension. + PowerShell, + + /// Python script + /// + /// A shim will be treated as a Python script if it has a `.py` file + /// extension. + Python, +} + +impl Shim<'_> { + pub fn new(def: Vec<&str>) -> Shim { + let length = def.len(); + assert_ne!(length, 0); + + let real_name = def[0]; + let name = if length == 1 { + internal::path::leaf_base(real_name).unwrap_or(real_name) + } else { + def[1] + }; + + let args = if length < 2 { + None + } else { + Some(def[2..].to_vec()) + }; + + let ty = Path::new(real_name) + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| match ext.to_lowercase().as_str() { + "bat" | "cmd" => ShimType::Batch, + "exe" | "com" => ShimType::Exe, + "jar" => ShimType::Java, + "ps1" => ShimType::PowerShell, + "py" => ShimType::Python, + _ => ShimType::Bash, + }) + .unwrap_or(ShimType::Bash); + + Shim { + name, + real_name, + ty, + args, + } + } +} + +// pub fn add(session: &Session, package: &Package) -> Fallible<()> { +// let config = session.config(); +// let shims_dir = config.root_path().join("shims"); + +// if let Some(bins) = package.manifest().bin() { +// // TODO +// } + +// Ok(()) +// } + +/// Remove shims for a package. +pub fn remove(session: &Session, package: &Package) -> Fallible<()> { + assert!(package.is_installed()); + + let config = session.config(); + let shims_dir = config.root_path().join("shims"); + + if let Some(bins) = package.manifest().bin() { + let pkg_name = package.name(); + let shims_dir_entries = shims_dir + .read_dir()? + .filter_map(Result::ok) + .collect::>(); + + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackageShimRemoveStart); + } + + for shim in bins.into_iter().map(Shim::new) { + let mut shim_path = shims_dir.join(shim.name); + let exts = match shim.ty { + ShimType::Exe => vec!["exe", "shim"], + ShimType::PowerShell => vec!["cmd", "ps1", ""], + _ => vec!["cmd", ""], + }; + + for ext in exts.into_iter() { + let alt_ext = format!("{}.{}", ext, pkg_name); + shim_path.set_extension(alt_ext); + + if shim_path.exists() { + if let Some(tx) = session.emitter() { + let shim_name = + shim_path.file_name().unwrap().to_string_lossy().to_string(); + let _ = tx.send(Event::PackageShimRemoveProgress(shim_name)); + } + + std::fs::remove_file(&shim_path)?; + } else { + // this is for removing the `pkg_name` suffix added by the + // `alt_ext` above + shim_path.set_extension(""); + + shim_path.set_extension(ext); + + if let Some(tx) = session.emitter() { + let shim_name = + shim_path.file_name().unwrap().to_string_lossy().to_string(); + let _ = tx.send(Event::PackageShimRemoveProgress(shim_name)); + } + + let _ = std::fs::remove_file(&shim_path); + + // restore alter shim + let fname = shim_path.file_name().unwrap().to_str().unwrap(); + let mut alt_shims = shims_dir_entries + .iter() + .flat_map(|entry| { + let path = entry.path(); + let name = path.file_name().unwrap().to_str().unwrap(); + + if name.starts_with(fname) && name != fname { + Some(entry) + } else { + None + } + }) + .collect::>(); + + if alt_shims.is_empty() { + continue; + } + + // sort by modified time, so the latest one will be used + // when there are multiple alter shims for the same shim + if alt_shims.len() > 1 { + alt_shims.sort_by_key(|de| { + std::cmp::Reverse(de.metadata().unwrap().modified().unwrap()) + }); + } + + let alt_shim = alt_shims.first().unwrap(); + let alt_path = alt_shim.path(); + let alt_path_new = alt_path.with_file_name(fname); + std::fs::rename(&alt_path, &alt_path_new)?; + } + } + } + + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackageShimRemoveDone); + } + } + + Ok(()) +} diff --git a/crates/libscoop/src/shortcut.rs b/crates/libscoop/src/shortcut.rs new file mode 100644 index 0000000..f505bb3 --- /dev/null +++ b/crates/libscoop/src/shortcut.rs @@ -0,0 +1,46 @@ +use once_cell::sync::Lazy; +use std::path::PathBuf; + +use crate::{error::Fallible, internal, package::Package, Event, Session}; + +static SCOOP_SHORTCUT_DIR: Lazy = Lazy::new(shortcut_dir); + +/// Return the path to the shortcut directory. +/// +/// `~\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Scoop Apps` +fn shortcut_dir() -> PathBuf { + let mut dir = dirs::config_dir().unwrap(); + dir.push("Microsoft/Windows/Start Menu/Programs/Scoop Apps"); + internal::path::normalize_path(dir) +} + +/// Remove shortcut(s) for a given package. +pub fn remove(session: &Session, package: &Package) -> Fallible<()> { + assert!(package.is_installed()); + + if let Some(shortcuts) = package.manifest().shortcuts() { + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackageShortcutRemoveStart); + } + + for shortcut in shortcuts { + let length = shortcut.len(); + assert!(length > 1); + + let mut path = SCOOP_SHORTCUT_DIR.join(shortcut[1]); + path.set_extension("lnk"); + + if let Some(tx) = session.emitter() { + let shortcut_name = path.file_name().unwrap().to_str().unwrap().to_owned(); + let _ = tx.send(Event::PackageShortcutRemoveProgress(shortcut_name)); + } + + let _ = std::fs::remove_file(&path); + } + + if let Some(tx) = session.emitter() { + let _ = tx.send(Event::PackageShortcutRemoveDone); + } + } + Ok(()) +}