Skip to content

Commit 9ed6252

Browse files
committed
handle pre_remove hook
1 parent 32a8aad commit 9ed6252

8 files changed

Lines changed: 191 additions & 70 deletions

File tree

crates/soar-cli/src/apply.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::{
2525
install::{create_install_context, perform_installation},
2626
state::AppState,
2727
update::perform_update,
28-
utils::{display_settings, icon_or, Colored, Icons},
28+
utils::{display_settings, get_package_hooks, icon_or, Colored, Icons},
2929
};
3030

3131
/// Result of comparing declared packages vs installed packages
@@ -547,8 +547,12 @@ async fn execute_apply(state: &AppState, diff: ApplyDiff, no_verify: bool) -> So
547547
info!("\nRemoving {} package(s)...", diff.to_remove.len());
548548

549549
for pkg in diff.to_remove {
550+
// Look up hooks from packages config (may not exist for pruned packages)
551+
let (hooks, sandbox) = get_package_hooks(&pkg.pkg_name);
550552
match PackageRemover::new(pkg.clone(), diesel_db.clone())
551553
.await
554+
.with_hooks(hooks)
555+
.with_sandbox(sandbox)
552556
.remove()
553557
.await
554558
{

crates/soar-cli/src/health.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use tracing::info;
1717

1818
use crate::{
1919
state::AppState,
20-
utils::{icon_or, term_width, Colored, Icons},
20+
utils::{get_package_hooks, icon_or, term_width, Colored, Icons},
2121
};
2222

2323
pub async fn display_health() -> SoarResult<()> {
@@ -156,8 +156,12 @@ pub async fn remove_broken_packages() -> SoarResult<()> {
156156
for package in broken_packages {
157157
let pkg_name = package.pkg_name.clone();
158158
let pkg_id = package.pkg_id.clone();
159+
let (hooks, sandbox) = get_package_hooks(&pkg_name);
159160
let installed_pkg = package.into();
160-
let remover = PackageRemover::new(installed_pkg, diesel_db.clone()).await;
161+
let remover = PackageRemover::new(installed_pkg, diesel_db.clone())
162+
.await
163+
.with_hooks(hooks)
164+
.with_sandbox(sandbox);
161165
remover.remove().await?;
162166

163167
info!("Removed {}#{}", pkg_name, pkg_id);

crates/soar-cli/src/remove.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use tracing::{debug, error, info, trace, warn};
88

99
use crate::{
1010
state::AppState,
11-
utils::{confirm_action, select_package_interactively, Colored},
11+
utils::{confirm_action, get_package_hooks, select_package_interactively, Colored},
1212
};
1313

1414
pub async fn remove_packages(packages: &[String], yes: bool, all: bool) -> SoarResult<()> {
@@ -55,7 +55,11 @@ pub async fn remove_packages(packages: &[String], yes: bool, all: bool) -> SoarR
5555
pkg_id = pkg.pkg_id,
5656
"removing package variant"
5757
);
58-
let remover = PackageRemover::new(pkg.clone(), diesel_db.clone()).await;
58+
let (hooks, sandbox) = get_package_hooks(&pkg.pkg_name);
59+
let remover = PackageRemover::new(pkg.clone(), diesel_db.clone())
60+
.await
61+
.with_hooks(hooks)
62+
.with_sandbox(sandbox);
5963
remover.remove().await?;
6064

6165
info!(
@@ -154,7 +158,11 @@ pub async fn remove_packages(packages: &[String], yes: bool, all: bool) -> SoarR
154158
pkg_id = pkg.pkg_id,
155159
"removing package"
156160
);
157-
let remover = PackageRemover::new(pkg.clone(), diesel_db.clone()).await;
161+
let (hooks, sandbox) = get_package_hooks(&pkg.pkg_name);
162+
let remover = PackageRemover::new(pkg.clone(), diesel_db.clone())
163+
.await
164+
.with_hooks(hooks)
165+
.with_sandbox(sandbox);
158166
remover.remove().await?;
159167

160168
info!(
@@ -218,7 +226,11 @@ pub async fn remove_packages(packages: &[String], yes: bool, all: bool) -> SoarR
218226
installed_path = installed_pkg.installed_path,
219227
"removing package"
220228
);
221-
let remover = PackageRemover::new(installed_pkg.clone(), diesel_db.clone()).await;
229+
let (hooks, sandbox) = get_package_hooks(&installed_pkg.pkg_name);
230+
let remover = PackageRemover::new(installed_pkg.clone(), diesel_db.clone())
231+
.await
232+
.with_hooks(hooks)
233+
.with_sandbox(sandbox);
222234
remover.remove().await?;
223235

224236
info!(

crates/soar-cli/src/utils.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ use indicatif::HumanBytes;
1212
use nu_ansi_term::Color::{self, Blue, Cyan, Green, LightRed, Magenta, Red};
1313
use serde::Serialize;
1414
use soar_config::{
15-
config::get_config, display::DisplaySettings, packages::BinaryMapping,
15+
config::get_config,
16+
display::DisplaySettings,
17+
packages::{BinaryMapping, PackageHooks, PackagesConfig, SandboxConfig},
1618
repository::get_platform_repositories,
1719
};
1820
use soar_core::{
@@ -520,3 +522,19 @@ pub fn parse_default_repos_arg(arg: &str) -> SoarResult<String> {
520522
)))
521523
}
522524
}
525+
526+
/// Look up hooks and sandbox configuration for a package from packages.toml.
527+
/// Returns (None, None) if packages.toml doesn't exist or the package isn't found.
528+
pub fn get_package_hooks(pkg_name: &str) -> (Option<PackageHooks>, Option<SandboxConfig>) {
529+
let config = match PackagesConfig::load(None) {
530+
Ok(c) => c,
531+
Err(_) => return (None, None),
532+
};
533+
534+
config
535+
.resolved_packages()
536+
.into_iter()
537+
.find(|p| p.name == pkg_name)
538+
.map(|p| (p.hooks, p.sandbox))
539+
.unwrap_or((None, None))
540+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use std::path::Path;
2+
3+
use soar_config::{config::get_config, packages::SandboxConfig};
4+
use tracing::{debug, warn};
5+
6+
use crate::{error::ErrorContext, sandbox, SoarError, SoarResult};
7+
8+
/// Environment variables passed to hook commands.
9+
pub struct HookEnv<'a> {
10+
pub install_dir: &'a Path,
11+
pub pkg_name: &'a str,
12+
pub pkg_id: &'a str,
13+
pub pkg_version: &'a str,
14+
}
15+
16+
/// Run a hook command with environment variables set.
17+
///
18+
/// This is the shared hook execution logic used by both package installation
19+
/// and removal operations.
20+
pub fn run_hook(
21+
hook_name: &str,
22+
command: &str,
23+
env: &HookEnv,
24+
sandbox_config: Option<&SandboxConfig>,
25+
) -> SoarResult<()> {
26+
debug!("running {} hook: {}", hook_name, command);
27+
28+
let bin_dir = get_config().get_bin_path()?;
29+
30+
let env_vars: Vec<(&str, &str)> = vec![
31+
("INSTALL_DIR", env.install_dir.to_str().unwrap_or("")),
32+
("BIN_DIR", bin_dir.to_str().unwrap_or("")),
33+
("PKG_NAME", env.pkg_name),
34+
("PKG_ID", env.pkg_id),
35+
("PKG_VERSION", env.pkg_version),
36+
];
37+
38+
let status = if sandbox::is_landlock_supported() {
39+
debug!("running {} hook with Landlock sandbox", hook_name);
40+
let mut cmd = sandbox::SandboxedCommand::new(command)
41+
.working_dir(env.install_dir)
42+
.read_path(&bin_dir)
43+
.envs(env_vars);
44+
45+
if let Some(s) = sandbox_config {
46+
let config = sandbox::SandboxConfig::new().with_network(if s.network {
47+
sandbox::NetworkConfig::allow_all()
48+
} else {
49+
sandbox::NetworkConfig::default()
50+
});
51+
cmd = cmd.config(config);
52+
for path in &s.fs_read {
53+
cmd = cmd.read_path(path);
54+
}
55+
for path in &s.fs_write {
56+
cmd = cmd.write_path(path);
57+
}
58+
}
59+
cmd.run()?
60+
} else {
61+
use std::process::Command;
62+
warn!(
63+
"Landlock not supported, running {} hook without sandbox",
64+
hook_name
65+
);
66+
Command::new("sh")
67+
.arg("-c")
68+
.arg(command)
69+
.env("INSTALL_DIR", env.install_dir)
70+
.env("BIN_DIR", &bin_dir)
71+
.env("PKG_NAME", env.pkg_name)
72+
.env("PKG_ID", env.pkg_id)
73+
.env("PKG_VERSION", env.pkg_version)
74+
.current_dir(env.install_dir)
75+
.status()
76+
.with_context(|| format!("executing {} hook", hook_name))?
77+
};
78+
79+
if !status.success() {
80+
return Err(SoarError::Custom(format!(
81+
"{} hook failed with exit code: {}",
82+
hook_name,
83+
status.code().unwrap_or(-1)
84+
)));
85+
}
86+
87+
Ok(())
88+
}

crates/soar-core/src/package/install.rs

Lines changed: 7 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -210,70 +210,16 @@ impl PackageInstaller {
210210

211211
/// Run a hook command with environment variables set.
212212
fn run_hook(&self, hook_name: &str, command: &str) -> SoarResult<()> {
213-
use crate::sandbox;
214-
215-
debug!("running {} hook: {}", hook_name, command);
216-
217-
let bin_dir = get_config().get_bin_path()?;
213+
use super::hooks::{run_hook, HookEnv};
218214

219-
let env_vars: Vec<(&str, &str)> = vec![
220-
("INSTALL_DIR", self.install_dir.to_str().unwrap_or("")),
221-
("BIN_DIR", bin_dir.to_str().unwrap_or("")),
222-
("PKG_NAME", &self.package.pkg_name),
223-
("PKG_ID", &self.package.pkg_id),
224-
("PKG_VERSION", &self.package.version),
225-
];
226-
227-
let status = if sandbox::is_landlock_supported() {
228-
debug!("running {} hook with Landlock sandbox", hook_name);
229-
let mut cmd = sandbox::SandboxedCommand::new(command)
230-
.working_dir(&self.install_dir)
231-
.read_path(&bin_dir)
232-
.envs(env_vars);
233-
234-
if let Some(s) = &self.sandbox {
235-
let config = sandbox::SandboxConfig::new().with_network(if s.network {
236-
sandbox::NetworkConfig::allow_all()
237-
} else {
238-
sandbox::NetworkConfig::default()
239-
});
240-
cmd = cmd.config(config);
241-
for path in &s.fs_read {
242-
cmd = cmd.read_path(path);
243-
}
244-
for path in &s.fs_write {
245-
cmd = cmd.write_path(path);
246-
}
247-
}
248-
cmd.run()?
249-
} else {
250-
use std::process::Command;
251-
warn!(
252-
"Landlock not supported, running {} hook without sandbox",
253-
hook_name
254-
);
255-
Command::new("sh")
256-
.arg("-c")
257-
.arg(command)
258-
.env("INSTALL_DIR", &self.install_dir)
259-
.env("BIN_DIR", &bin_dir)
260-
.env("PKG_NAME", &self.package.pkg_name)
261-
.env("PKG_ID", &self.package.pkg_id)
262-
.env("PKG_VERSION", &self.package.version)
263-
.current_dir(&self.install_dir)
264-
.status()
265-
.with_context(|| format!("executing {} hook", hook_name))?
215+
let env = HookEnv {
216+
install_dir: &self.install_dir,
217+
pkg_name: &self.package.pkg_name,
218+
pkg_id: &self.package.pkg_id,
219+
pkg_version: &self.package.version,
266220
};
267221

268-
if !status.success() {
269-
return Err(SoarError::Custom(format!(
270-
"{} hook failed with exit code: {}",
271-
hook_name,
272-
status.code().unwrap_or(-1)
273-
)));
274-
}
275-
276-
Ok(())
222+
run_hook(hook_name, command, &env, self.sandbox.as_ref())
277223
}
278224

279225
/// Run post_download hook if configured.

crates/soar-core/src/package/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod hooks;
12
pub mod install;
23
pub mod query;
34
pub mod remote_update;

crates/soar-core/src/package/remove.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ use std::{
44
path::{Path, PathBuf},
55
};
66

7-
use soar_config::config::get_config;
7+
use soar_config::{
8+
config::get_config,
9+
packages::{PackageHooks, SandboxConfig},
10+
};
811
use soar_db::{models::types::ProvideStrategy, repository::core::CoreRepository};
912
use soar_utils::{error::FileSystemResult, fs::walk_dir, path::desktop_dir};
1013
use tracing::{debug, trace, warn};
1114

15+
use super::hooks::{run_hook, HookEnv};
16+
1217
/// Formats bytes into human-readable string (e.g., "1.5 MiB")
1318
fn format_size(bytes: u64) -> String {
1419
const KIB: u64 = 1024;
@@ -35,6 +40,8 @@ use crate::{
3540
pub struct PackageRemover {
3641
package: InstalledPackage,
3742
db: DieselDatabase,
43+
hooks: Option<PackageHooks>,
44+
sandbox: Option<SandboxConfig>,
3845
}
3946

4047
impl PackageRemover {
@@ -47,9 +54,47 @@ impl PackageRemover {
4754
Self {
4855
package,
4956
db,
57+
hooks: None,
58+
sandbox: None,
5059
}
5160
}
5261

62+
/// Set hooks configuration for the package removal.
63+
pub fn with_hooks(mut self, hooks: Option<PackageHooks>) -> Self {
64+
self.hooks = hooks;
65+
self
66+
}
67+
68+
/// Set sandbox configuration for hook execution.
69+
pub fn with_sandbox(mut self, sandbox: Option<SandboxConfig>) -> Self {
70+
self.sandbox = sandbox;
71+
self
72+
}
73+
74+
/// Run a hook command with environment variables set.
75+
fn run_hook(&self, hook_name: &str, command: &str) -> SoarResult<()> {
76+
let install_dir = PathBuf::from(&self.package.installed_path);
77+
let env = HookEnv {
78+
install_dir: &install_dir,
79+
pkg_name: &self.package.pkg_name,
80+
pkg_id: &self.package.pkg_id,
81+
pkg_version: &self.package.version,
82+
};
83+
84+
run_hook(hook_name, command, &env, self.sandbox.as_ref())
85+
}
86+
87+
/// Run pre_remove hook if configured.
88+
/// This should be called before any file deletions during package removal.
89+
pub fn run_pre_remove_hook(&self) -> SoarResult<()> {
90+
if let Some(ref hooks) = self.hooks {
91+
if let Some(ref cmd) = hooks.pre_remove {
92+
self.run_hook("pre_remove", cmd)?;
93+
}
94+
}
95+
Ok(())
96+
}
97+
5398
pub async fn remove(&self) -> SoarResult<()> {
5499
debug!(
55100
pkg_name = self.package.pkg_name,
@@ -63,6 +108,9 @@ impl PackageRemover {
63108
self.package.repo_name,
64109
self.package.version
65110
);
111+
112+
self.run_pre_remove_hook()?;
113+
66114
// Track removed symlinks for logging
67115
let mut removed_symlinks: Vec<PathBuf> = Vec::new();
68116

0 commit comments

Comments
 (0)