use std::fs;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use color_eyre::Result;
use color_eyre::eyre::{self, WrapErr};
use log;
use crate::lfs;
fn adjust_exec_line(exec_line: &str, env_root: &Path) -> Result<String> {
let (prefix, rest) = exec_line.split_once('=')
.ok_or_else(|| eyre::eyre!("Invalid Exec line: {}", exec_line))?;
let command_end = rest.find(char::is_whitespace).unwrap_or(rest.len());
let command = &rest[..command_end];
let args = &rest[command_end..];
let binary_name = Path::new(command)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| command.to_string());
let ebin_wrapper = env_root.join("ebin").join(&binary_name);
let new_command = if ebin_wrapper.exists() {
ebin_wrapper.to_string_lossy().to_string()
} else {
log::debug!("No ebin wrapper found for {}, using original command", binary_name);
command.to_string()
};
Ok(format!("{}={}{}", prefix, new_command, args))
}
fn adjust_icon_line(icon_line: &str) -> Result<String> {
let (prefix, value) = icon_line.split_once('=')
.ok_or_else(|| eyre::eyre!("Invalid Icon line: {}", icon_line))?;
if value.starts_with("/usr/share/icons/") {
let path = Path::new(value);
if let Some(stem) = path.file_stem() {
return Ok(format!("{}={}", prefix, stem.to_string_lossy()));
}
}
Ok(icon_line.to_string())
}
fn adjust_desktop_file(desktop_path: &Path, env_root: &Path) -> Result<String> {
let file = fs::File::open(desktop_path)
.with_context(|| format!("Failed to open desktop file {}", desktop_path.display()))?;
let reader = BufReader::new(file);
let mut output = String::new();
for line in reader.lines() {
let line = line?;
if line.starts_with("Exec=") {
let adjusted = adjust_exec_line(&line, env_root)?;
output.push_str(&adjusted);
output.push('\n');
} else if line.starts_with("Icon=") {
let adjusted = adjust_icon_line(&line)?;
output.push_str(&adjusted);
output.push('\n');
} else {
output.push_str(&line);
output.push('\n');
}
}
Ok(output)
}
fn create_desktop_files(
env_root: &Path,
filelist: &[String],
src_prefix: &str,
dst_subdir: &str,
) -> Result<Vec<PathBuf>> {
let mut processed_files = Vec::new();
let home = crate::dirs::get_home().ok();
let home_path = if let Some(ref home_str) = home {
Path::new(home_str)
} else {
return Ok(processed_files);
};
let dst_dir = home_path.join(dst_subdir);
lfs::create_dir_all(&dst_dir)?;
let desktop_files: Vec<&String> = filelist
.iter()
.filter(|path| {
path.starts_with(src_prefix) &&
path.ends_with(".desktop")
})
.collect();
for file_path in desktop_files {
let host_rel = lfs::host_path_from_manifest_rel_path(file_path.trim_start_matches('/'));
let filename = host_rel
.file_name()
.ok_or_else(|| eyre::eyre!("Failed to get filename for {}", file_path))?;
let dst_path = dst_dir.join(filename);
let src_path = env_root.join(&host_rel);
let adjusted_content = adjust_desktop_file(&src_path, env_root)?;
lfs::write(&dst_path, adjusted_content)?;
processed_files.push(dst_path.clone());
log::debug!("Processed desktop file: {} -> {}", src_path.display(), dst_path.display());
}
Ok(processed_files)
}
fn symlink_desktop_files(
env_root: &Path,
filelist: &[String],
src_prefix: &str,
dst_subdir: &str,
) -> Result<Vec<PathBuf>> {
let mut linked_items = Vec::new();
let home = crate::dirs::get_home().ok();
let home_path = if let Some(ref home_str) = home {
Path::new(home_str)
} else {
return Ok(linked_items);
};
let dst_dir = home_path.join(dst_subdir);
lfs::create_dir_all(&dst_dir)?;
let matching_items: Vec<&String> = filelist
.iter()
.filter(|path| path.starts_with(src_prefix))
.collect();
for file_path in matching_items {
let relative_path = &file_path[src_prefix.len()..];
let relative_path = relative_path.strip_prefix('/').unwrap_or(relative_path);
let dst_path = dst_dir.join(crate::lfs::host_path_from_manifest_rel_path(relative_path));
if let Some(parent) = dst_path.parent() {
crate::utils::safe_mkdir_p(parent)?;
}
if lfs::is_symlink(&dst_path) {
lfs::remove_file(&dst_path)?;
} else if dst_path.exists() {
continue;
}
let target_path = env_root.join(crate::lfs::host_path_from_manifest_rel_path(file_path));
if target_path.is_dir() {
continue;
}
lfs::symlink_file_for_virtiofs(&target_path, &dst_path)?;
linked_items.push(dst_path.clone());
log::debug!("Linked desktop file: {} -> {}", target_path.display(), dst_path.display());
}
Ok(linked_items)
}
fn run_desktop_update_command(
home: &Path,
env_root: &Path,
command: &str,
args: &[&str],
subdir: &str,
command_name: &str,
) -> Result<()> {
let xdg_data_home = crate::dirs::path_join(home, &[".local", "share"]);
let dir_to_update = xdg_data_home.join(subdir);
if !dir_to_update.exists() {
log::debug!("{} directory {} does not exist, skipping {}", command_name, dir_to_update.display(), command);
return Ok(());
}
let mut run_options = crate::run::RunOptions {
command: command.to_string(),
args: args.iter().map(|s| s.to_string()).collect(),
..Default::default()
};
run_options.args.push(dir_to_update.display().to_string());
run_options.env_vars.insert("XDG_DATA_HOME".to_string(), xdg_data_home.to_string_lossy().to_string());
match crate::run::fork_and_execute(env_root, &run_options) {
Ok(_) => {
log::debug!("Updated {}", command_name.to_lowercase());
}
Err(e) => {
if e.to_string().contains("not found") || e.to_string().contains("ENOENT") {
run_options.skip_namespace_isolation = true;
match crate::run::fork_and_execute(Path::new("/"), &run_options) {
Ok(_) => {
log::debug!("Updated {} (retried with /)", command_name.to_lowercase());
}
Err(retry_e) => {
log::info!("{} not work even after retry: {}", command, retry_e);
}
}
} else {
log::warn!("{} failed: {}", command, e);
}
}
}
Ok(())
}
fn update_desktop_database(env_root: &Path, home: &Path) -> Result<()> {
run_desktop_update_command(
home,
env_root,
"update-desktop-database",
&[],
"applications",
"desktop database"
)
}
fn update_icon_cache(env_root: &Path, home: &Path) -> Result<()> {
run_desktop_update_command(
home,
env_root,
"gtk-update-icon-cache",
&["-f", "-t"],
"icons/hicolor",
"icon cache"
)
}
fn update_mime_database(env_root: &Path, home: &Path) -> Result<()> {
run_desktop_update_command(
home,
env_root,
"update-mime-database",
&[],
"mime",
"MIME database"
)
}
fn update_font_cache(env_root: &Path, home: &Path) -> Result<()> {
run_desktop_update_command(
home,
env_root,
"fc-cache",
&["-f"],
"fonts",
"font cache"
)
}
#[derive(Debug, Clone, Default)]
pub struct DesktopIntegrationFlags {
pub desktop_files: bool,
pub icons: bool,
pub mime_files: bool,
pub fonts: bool,
}
pub fn expose_package_xdesktop(env_root: &Path, filelist: &[String], desktop_integration_occurred: &mut DesktopIntegrationFlags) -> Result<Vec<String>> {
let mut links = Vec::new();
macro_rules! collect_integration_links {
($flag_field:ident, $func:expr) => {
let processed_files = $func?;
if !processed_files.is_empty() {
desktop_integration_occurred.$flag_field = true;
}
links.extend(processed_files.into_iter().map(|p| p.to_string_lossy().into_owned()));
};
}
collect_integration_links!(desktop_files, create_desktop_files(env_root, filelist, "etc/xdg/autostart", ".config/autostart"));
collect_integration_links!(desktop_files, create_desktop_files(env_root, filelist, "usr/share/applications", ".local/share/applications"));
collect_integration_links!(icons, symlink_desktop_files(env_root, filelist, "usr/share/icons", ".local/share/icons"));
collect_integration_links!(fonts, symlink_desktop_files(env_root, filelist, "usr/share/fonts", ".local/share/fonts"));
collect_integration_links!(mime_files, symlink_desktop_files(env_root, filelist, "usr/share/mime/packages", ".local/share/mime/packages"));
let _ = symlink_desktop_files(env_root, filelist, "usr/share/dbus-1/services", ".local/share/dbus-1/services");
Ok(links)
}
fn is_env_desktop_file(desktop_path: &Path, env_root: &Path) -> Result<bool> {
let file = match fs::File::open(desktop_path) {
Ok(f) => f,
Err(_) => return Ok(false),
};
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
if line.starts_with("Exec=") {
let env_prefix = env_root.to_string_lossy().to_string();
if line.contains(&env_prefix) {
return Ok(true);
}
break;
}
}
Ok(false)
}
fn remove_symlinked_file(
link_path: &Path,
link_path_str: &str,
env_root: &Path,
desktop_integration_occurred: &mut DesktopIntegrationFlags,
) -> Result<()> {
if let Ok(target_path) = fs::read_link(link_path) {
if target_path.starts_with(env_root) {
if let Err(e) = lfs::remove_file(link_path) {
log::warn!("Failed to remove desktop integration file {}: {}", link_path.display(), e);
} else {
log::debug!("Removed desktop integration file: {}", link_path.display());
if link_path_str.contains("icons") {
desktop_integration_occurred.icons = true;
} else if link_path_str.contains("fonts") {
desktop_integration_occurred.fonts = true;
} else if link_path_str.contains("mime") {
desktop_integration_occurred.mime_files = true;
}
}
} else {
log::warn!("Desktop integration file {} points outside environment or store ({}), not removing",
link_path.display(), target_path.display());
}
} else {
log::warn!("Desktop integration file {} is not a symlink or cannot read link target, not removing",
link_path.display());
}
Ok(())
}
fn remove_desktop_file(
link_path: &Path,
env_root: &Path,
desktop_integration_occurred: &mut DesktopIntegrationFlags,
) -> Result<()> {
match is_env_desktop_file(link_path, env_root) {
Ok(true) => {
if let Err(e) = lfs::remove_file(link_path) {
log::warn!("Failed to remove desktop integration file {}: {}", link_path.display(), e);
} else {
log::debug!("Removed desktop integration file: {}", link_path.display());
desktop_integration_occurred.desktop_files = true;
}
}
Ok(false) => {
log::warn!("Desktop file {} not pointing to env_root {}, not removing",
link_path.display(),
env_root.display(),
);
}
Err(e) => {
log::warn!("Failed to check if desktop file {} was created by epkg: {}, not removing",
link_path.display(), e);
}
}
Ok(())
}
pub fn unexpose_package_xdesktop(
xdesktop_links: &[String],
env_root: &Path,
desktop_integration_occurred: &mut DesktopIntegrationFlags,
) -> Result<()> {
for link_path_str in xdesktop_links {
let link_path = Path::new(link_path_str);
if !link_path.exists() {
log::debug!("Desktop integration file does not exist, skipping: {}", link_path.display());
continue;
}
if lfs::is_symlink(link_path) {
remove_symlinked_file(link_path, link_path_str, env_root, desktop_integration_occurred)?;
} else if link_path_str.ends_with(".desktop") {
remove_desktop_file(link_path, env_root, desktop_integration_occurred)?;
} else {
log::warn!("Desktop integration file {} is not a symlink or .desktop file, not removing", link_path.display());
}
}
Ok(())
}
pub fn update_desktop_databases(env_root: &Path, desktop_integration_occurred: &DesktopIntegrationFlags) {
macro_rules! update_database_if_needed {
($flag_field:ident, $update_func:expr) => {
if desktop_integration_occurred.$flag_field {
let _ = $update_func;
}
};
}
let home = crate::dirs::get_home().ok();
if let Some(home_path) = home {
let home_path = Path::new(&home_path);
update_database_if_needed!(desktop_files, update_desktop_database(env_root, home_path));
update_database_if_needed!(icons, update_icon_cache(env_root, home_path));
update_database_if_needed!(fonts, update_font_cache(env_root, home_path));
update_database_if_needed!(mime_files, update_mime_database(env_root, home_path));
}
}