Skip to content

Commit 23eeb04

Browse files
committed
Refactor host env lookup to return an hashmap
1 parent 3b617fb commit 23eeb04

4 files changed

Lines changed: 77 additions & 103 deletions

File tree

src/backends/distrobox/distrobox.rs

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::fakers::{Child, Command, CommandRunner, FdMode, NullCommandRunnerBuil
33
use serde::{Deserialize, Deserializer};
44
use std::{
55
cell::LazyCell,
6-
collections::BTreeMap,
6+
collections::{BTreeMap, HashMap},
77
ffi::OsString,
88
io,
99
os::unix::ffi::OsStringExt,
@@ -658,46 +658,40 @@ impl Distrobox {
658658
Ok(s.to_string())
659659
}
660660

661-
async fn host_applications_path(&self) -> Result<PathBuf, Error> {
662-
// Resolve XDG_DATA_HOME via runner (works in Flatpak via map_flatpak_spawn_host)
663-
let xdg_data_home_opt =
664-
match crate::fakers::resolve_host_env_via_runner(&self.cmd_runner, "XDG_DATA_HOME")
665-
.await
666-
{
667-
Ok(Some(s)) if !s.trim().is_empty() => Some(Path::new(s.trim()).to_path_buf()),
668-
Ok(_) => None,
669-
Err(e) => {
670-
tracing::warn!("failed to resolve XDG_DATA_HOME via CommandRunner: {e:?}");
671-
None
672-
}
673-
};
661+
async fn host_applications_path(
662+
&self,
663+
host_env: &HashMap<String, String>,
664+
) -> Result<PathBuf, Error> {
665+
let xdg_data_home_opt = host_env
666+
.get("XDG_DATA_HOME")
667+
.filter(|s| !s.trim().is_empty())
668+
.map(|s| Path::new(s.trim()).to_path_buf());
674669

675670
let apps_base = if let Some(p) = xdg_data_home_opt {
676671
p
677672
} else {
678673
// Fallback to HOME
679-
match crate::fakers::resolve_host_env_via_runner(&self.cmd_runner, "HOME").await {
680-
Ok(Some(s)) if !s.trim().is_empty() => Path::new(s.trim()).join(".local/share"),
681-
Ok(_) => {
674+
match host_env.get("HOME").filter(|s| !s.trim().is_empty()) {
675+
Some(s) => Path::new(s.trim()).join(".local/share"),
676+
None => {
682677
return Err(Error::ResolveHostPath(
683678
"XDG_DATA_HOME and HOME are not set on the host".into(),
684679
));
685680
}
686-
Err(e) => {
687-
tracing::warn!("failed to resolve HOME via CommandRunner: {e:?}");
688-
return Err(Error::ResolveHostPath("failed to resolve host HOME".into()));
689-
}
690681
}
691682
};
692683

693684
let apps_path = apps_base.join("applications");
694685
Ok(apps_path)
695686
}
696-
async fn get_exported_desktop_files(&self) -> Result<Vec<String>, Error> {
687+
async fn get_exported_desktop_files(
688+
&self,
689+
host_env: &HashMap<String, String>,
690+
) -> Result<Vec<String>, Error> {
697691
// We do everything with the command line to ensure we can access the files and environment variables
698692
// even when inside a flatpak sandbox, with only the permissions to run `flatpak-spawn`
699693
let mut cmd = Command::new("ls");
700-
cmd.arg(self.host_applications_path().await?);
694+
cmd.arg(self.host_applications_path(host_env).await?);
701695
let ls_out = self.cmd_output_string(cmd).await?;
702696
let apps = ls_out
703697
.trim()
@@ -707,7 +701,11 @@ impl Distrobox {
707701
Ok(apps)
708702
}
709703

710-
async fn get_desktop_files(&self, box_name: &str) -> Result<Vec<(String, String)>, Error> {
704+
async fn get_desktop_files(
705+
&self,
706+
box_name: &str,
707+
host_env: &HashMap<String, String>,
708+
) -> Result<Vec<(String, String)>, Error> {
711709
let mut cmd = self.dbcmd();
712710
cmd.args([
713711
"enter",
@@ -721,16 +719,7 @@ impl Distrobox {
721719
.map_err(|e| Error::ParseOutput(format!("{e:?}")))?;
722720
debug!(desktop_files = format_args!("{desktop_files:#?}"));
723721

724-
// Resolve host HOME via CommandRunner so this works inside Flatpak as well
725-
let host_home_opt =
726-
match crate::fakers::resolve_host_env_via_runner(&self.cmd_runner, "HOME").await {
727-
Ok(Some(s)) => Some(PathBuf::from(s)),
728-
Ok(None) => None,
729-
Err(e) => {
730-
tracing::warn!("failed to resolve host HOME via CommandRunner: {e:?}");
731-
None
732-
}
733-
};
722+
let host_home_opt = host_env.get("HOME").cloned().map(PathBuf::from);
734723

735724
Ok(desktop_files
736725
.into_map(host_home_opt)
@@ -740,9 +729,17 @@ impl Distrobox {
740729
}
741730

742731
pub async fn list_apps(&self, box_name: &str) -> Result<Vec<ExportableApp>, Error> {
743-
let files = self.get_desktop_files(box_name).await?;
732+
let host_env = match crate::fakers::resolve_host_env(&self.cmd_runner).await {
733+
Ok(env) => env,
734+
Err(e) => {
735+
tracing::warn!("failed to resolve host env via CommandRunner: {e:?}");
736+
HashMap::new()
737+
}
738+
};
739+
740+
let files = self.get_desktop_files(box_name, &host_env).await?;
744741
debug!(desktop_files=?files);
745-
let exported = self.get_exported_desktop_files().await?;
742+
let exported = self.get_exported_desktop_files(&host_env).await?;
746743
debug!(exported_files=?exported);
747744
let res: Vec<ExportableApp> = files
748745
.into_iter()

src/fakers/host_env.rs

Lines changed: 35 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,9 @@
11
use crate::fakers::{Command, CommandRunner};
2+
use std::collections::HashMap;
23
use std::io;
34

4-
/// Resolve an environment variable on the host via a `CommandRunner`.
5-
///
6-
/// Returns `Ok(Some(value))` if present, `Ok(None)` if unset/empty, or `Err(e)` on I/O/command errors.
7-
pub async fn resolve_host_env_via_runner(
8-
runner: &CommandRunner,
9-
key: &str,
10-
) -> io::Result<Option<String>> {
11-
// Try `printenv KEY` first — simpler when available
12-
let cmd = Command::new_with_args("printenv", [key]);
13-
match runner.output_string(cmd.clone()).await {
14-
Ok(out) => {
15-
let trimmed = out.trim().to_string();
16-
if !trimmed.is_empty() {
17-
return Ok(Some(trimmed));
18-
}
19-
// fallthrough to sh -c printf if empty
20-
}
21-
Err(_) => {
22-
// fallback below
23-
}
24-
}
25-
26-
// Fallback: sh -c 'printf "%s" "$KEY"'
27-
let mut cmd = Command::new("sh");
28-
cmd.arg("-c");
29-
// Only interpolate the variable name to avoid injection
30-
cmd.arg(format!("printf '%s' \"${}\"", key));
31-
32-
let out = runner.output_string(cmd).await?;
33-
let trimmed = out.trim().to_string();
34-
if trimmed.is_empty() {
35-
Ok(None)
36-
} else {
37-
Ok(Some(trimmed))
38-
}
39-
}
40-
41-
/// Resolve all host environment variables via a `CommandRunner` as `KEY=VALUE` entries.
42-
///
43-
/// This is useful when spawning processes that need the host `PATH` and other vars, e.g. VTE.
44-
pub async fn resolve_host_env_list_via_runner(runner: &CommandRunner) -> io::Result<Vec<String>> {
5+
/// Resolve all host environment variables via a `CommandRunner`.
6+
pub async fn resolve_host_env(runner: &CommandRunner) -> io::Result<HashMap<String, String>> {
457
// Prefer NUL-separated output to preserve values containing newlines.
468
let mut cmd = Command::new("env");
479
cmd.arg("-0");
@@ -56,9 +18,9 @@ pub async fn resolve_host_env_list_via_runner(runner: &CommandRunner) -> io::Res
5618
return None;
5719
}
5820
let s = String::from_utf8_lossy(entry).to_string();
59-
if s.contains('=') { Some(s) } else { None }
21+
s.split_once('=').map(|(k, v)| (k.to_string(), v.to_string()))
6022
})
61-
.collect::<Vec<_>>();
23+
.collect::<HashMap<_, _>>();
6224
if !vars.is_empty() {
6325
return Ok(vars);
6426
}
@@ -73,43 +35,55 @@ pub async fn resolve_host_env_list_via_runner(runner: &CommandRunner) -> io::Res
7335

7436
Ok(String::from_utf8_lossy(&output.stdout)
7537
.lines()
76-
.filter(|line| line.contains('='))
77-
.map(ToString::to_string)
38+
.filter_map(|line| {
39+
line.split_once('=')
40+
.map(|(k, v)| (k.to_string(), v.to_string()))
41+
})
7842
.collect())
7943
}
8044

45+
pub fn host_env_to_list(env: &HashMap<String, String>) -> Vec<String> {
46+
let mut list = env
47+
.iter()
48+
.map(|(key, value)| format!("{key}={value}"))
49+
.collect::<Vec<_>>();
50+
list.sort_unstable();
51+
list
52+
}
53+
8154
#[cfg(test)]
8255
mod tests {
8356
use super::*;
8457
use crate::fakers::NullCommandRunnerBuilder;
8558
use smol::block_on;
8659

8760
#[test]
88-
fn test_resolve_host_env_with_null_runner() {
89-
let runner = NullCommandRunnerBuilder::new()
90-
.cmd(&["printenv", "HOME"], "/fake/home")
91-
.build();
92-
93-
let res = block_on(resolve_host_env_via_runner(&runner, "HOME")).unwrap();
94-
assert_eq!(res, Some("/fake/home".to_string()));
95-
}
96-
97-
#[test]
98-
fn test_resolve_host_env_unset_with_null_runner() {
61+
fn test_resolve_host_env_empty_with_null_runner() {
9962
let runner = NullCommandRunnerBuilder::new().build();
100-
let res = block_on(resolve_host_env_via_runner(&runner, "HOME")).unwrap();
101-
assert_eq!(res, None);
63+
let res = block_on(resolve_host_env(&runner)).unwrap();
64+
assert!(res.is_empty());
10265
}
10366

10467
#[test]
105-
fn test_resolve_host_env_list_with_null_runner_nul_separated() {
68+
fn test_resolve_host_env_with_null_runner_nul_separated() {
10669
let mut builder = NullCommandRunnerBuilder::new();
10770
let mut cmd = Command::new("env");
10871
cmd.arg("-0");
10972
builder.cmd_full(cmd, || Ok("HOME=/fake/home\0PATH=/nix/store/bin\0".to_string()));
11073
let runner = builder.build();
11174

112-
let res = block_on(resolve_host_env_list_via_runner(&runner)).unwrap();
113-
assert_eq!(res, vec!["HOME=/fake/home", "PATH=/nix/store/bin"]);
75+
let res = block_on(resolve_host_env(&runner)).unwrap();
76+
assert_eq!(res.get("HOME"), Some(&"/fake/home".to_string()));
77+
assert_eq!(res.get("PATH"), Some(&"/nix/store/bin".to_string()));
78+
}
79+
80+
#[test]
81+
fn test_host_env_to_list() {
82+
let env = HashMap::from([
83+
("PATH".to_string(), "/nix/store/bin".to_string()),
84+
("HOME".to_string(), "/fake/home".to_string()),
85+
]);
86+
let list = host_env_to_list(&env);
87+
assert_eq!(list, vec!["HOME=/fake/home", "PATH=/nix/store/bin"]);
11488
}
11589
}

src/fakers/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ mod output_tracker;
55

66
pub use command::{Command, FdMode};
77
pub use command_runner::{Child, CommandRunner, CommandRunnerEvent, NullCommandRunnerBuilder};
8-
pub use host_env::{resolve_host_env_list_via_runner, resolve_host_env_via_runner};
8+
pub use host_env::{host_env_to_list, resolve_host_env};
99
pub use output_tracker::OutputTracker;

src/widgets/integrated_terminal.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use gtk::{
66
};
77
use vte4::prelude::*;
88

9-
use crate::fakers::resolve_host_env_list_via_runner;
9+
use crate::fakers::{host_env_to_list, resolve_host_env};
1010
use crate::i18n::gettext;
1111
use crate::gtk_utils::ColorPalette;
1212
use crate::models::Container;
@@ -176,16 +176,19 @@ impl IntegratedTerminal {
176176
.filter_map(|s| s.to_str())
177177
.collect::<Vec<_>>();
178178

179-
let host_env = match resolve_host_env_list_via_runner(&command_runner).await {
179+
let host_env = match resolve_host_env(&command_runner).await {
180180
Ok(env) => env,
181181
Err(err) => {
182182
eprintln!("Failed to resolve host env for terminal: {}", err);
183-
Vec::new()
183+
std::collections::HashMap::new()
184184
}
185185
};
186-
let host_env_refs = host_env.iter().map(String::as_str).collect::<Vec<_>>();
186+
let host_env_list = host_env_to_list(&host_env);
187+
let host_env_refs = host_env_list
188+
.iter()
189+
.map(String::as_str)
190+
.collect::<Vec<_>>();
187191

188-
dbg!(&host_env_refs);
189192
let fut = this.imp().terminal.spawn_future(
190193
vte4::PtyFlags::DEFAULT,
191194
None,

0 commit comments

Comments
 (0)