// Copyright 2016-2019 Cargo-Bundle developers // Copyright 2019-2024 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! This module provides utilities helping the packaging of desktop //! applications for Linux: //! //! - Generation of [desktop entries] (`.desktop` files) //! - Copy of icons in the [icons file hierarchy] //! //! The specifications are developed and hosted at [freedesktop.org]. //! //! [freedesktop.org]: https://www.freedesktop.org //! [desktop entries]: https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/ //! [icons file hierarchy]: https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#icon_lookup use std::collections::BTreeMap; use std::ffi::OsStr; use std::fs::{File, read_to_string}; use std::io::BufReader; use std::path::{Path, PathBuf}; use handlebars::Handlebars; use image::{self, ImageDecoder, codecs::png::PngDecoder}; use serde::Serialize; use crate::{ Settings, error::Context, utils::{self, fs_utils}, }; #[derive(PartialEq, Eq, PartialOrd, Ord)] pub struct Icon { pub width: u32, pub height: u32, pub is_high_density: bool, pub path: PathBuf, } /// Generate the icon files, and returns a map where keys are the icons and /// values are their current (source) path. pub fn list_icon_files( settings: &Settings, data_dir: &Path, ) -> crate::Result> { let base_dir = data_dir.join("usr/share/icons/hicolor"); let main_binary_name = settings.main_binary_name()?; let get_dest_path = |width: u32, height: u32, is_high_density: bool| { base_dir.join(format!( "{}x{}{}/apps/{}.png", width, height, if is_high_density { "@2" } else { "" }, main_binary_name )) }; let mut icons = BTreeMap::new(); for icon_path in settings.icon_files() { let icon_path = icon_path?; if icon_path.extension() != Some(OsStr::new("png")) { continue; } // Put file in scope so that it's closed when copying it let icon = { let decoder = PngDecoder::new(BufReader::new(File::open(&icon_path)?))?; let width = decoder.dimensions().0; let height = decoder.dimensions().1; let is_high_density = utils::is_retina(&icon_path); let dest_path = get_dest_path(width, height, is_high_density); Icon { width, height, is_high_density, path: dest_path, } }; icons.entry(icon).or_insert(icon_path); } Ok(icons) } /// Generate the icon files and store them under the `data_dir`. pub fn copy_icon_files(settings: &Settings, data_dir: &Path) -> crate::Result> { let icons = list_icon_files(settings, data_dir)?; for (icon, src) in &icons { fs_utils::copy_file(src, &icon.path)?; } Ok(icons.into_keys().collect()) } /// Generate the application desktop file and store it under the `data_dir`. /// Returns the path of the resulting file (source path) and the destination /// path in the package. pub fn generate_desktop_file( settings: &Settings, custom_template_path: &Option, data_dir: &Path, ) -> crate::Result<(PathBuf, PathBuf)> { let bin_name = settings.main_binary_name()?; let product_name = settings.product_name(); let desktop_file_name = format!("{product_name}.desktop"); let path = PathBuf::from("usr/share/applications").join(desktop_file_name); let dest_path = PathBuf::from("/").join(&path); let file_path = data_dir.join(&path); let file = &mut fs_utils::create_file(&file_path)?; let mut handlebars = Handlebars::new(); handlebars.register_escape_fn(handlebars::no_escape); if let Some(template) = custom_template_path { handlebars .register_template_string("main.desktop", read_to_string(template)?) .map_err(Into::into) .context("Failed to setup custom handlebar template")?; } else { handlebars .register_template_string("main.desktop", include_str!("./main.desktop")) .map_err(Into::into) .context("Failed to setup default handlebar template")?; } #[derive(Serialize)] struct DesktopTemplateParams<'a> { categories: &'a str, comment: Option<&'a str>, exec: &'a str, icon: &'a str, name: &'a str, mime_type: Option, long_description: String, } let mut mime_type: Vec = Vec::new(); if let Some(associations) = settings.file_associations() { mime_type.extend( associations .iter() .filter_map(|association| association.mime_type.clone()), ); } if let Some(protocols) = settings.deep_link_protocols() { mime_type.extend( protocols .iter() .flat_map(|protocol| &protocol.schemes) .map(|s| format!("x-scheme-handler/{s}")), ); } let mime_type = (!mime_type.is_empty()).then_some(mime_type.join(";")); let bin_name_exec = if bin_name.contains(' ') { format!("\"{bin_name}\"") } else { bin_name.to_string() }; handlebars.render_to_write( "main.desktop", &DesktopTemplateParams { categories: settings .app_category() .map(|app_category| app_category.freedesktop_categories()) .unwrap_or(""), comment: if !settings.short_description().is_empty() { Some(settings.short_description()) } else { None }, exec: &bin_name_exec, icon: bin_name, name: settings.product_name(), mime_type, long_description: settings.long_description().unwrap_or_default().to_string(), }, file, )?; Ok((file_path, dest_path)) }