Эх сурвалжийг харах

feat(bundler): Add RPM packaging, closes #4402 (#5202)

* feat(bundler): Add RPM packaging

* feat(bundler): Update 'rpm' to 0.13.1

* Fix fmt
Olivier Lemasle 1 жил өмнө
parent
commit
091100acbb

+ 7 - 0
.changes/bundler-rpm.md

@@ -0,0 +1,7 @@
+---
+"tauri-bundler": 'patch:enhance'
+"tauri-cli": 'patch:enhance'
+"@tauri-apps/cli": 'patch:enhance'
+---
+
+Add RPM packaging

+ 1 - 1
README.md

@@ -31,7 +31,7 @@ npm create tauri-app@latest
 
 The list of Tauri's features includes, but is not limited to:
 
-- Built-in app bundler to create app bundles in formats like `.app`, `.dmg`, `.deb`, `.AppImage` and Windows installers like `.exe` (via NSIS) and `.msi` (via WiX).
+- Built-in app bundler to create app bundles in formats like `.app`, `.dmg`, `.deb`, `.rpm`, `.AppImage` and Windows installers like `.exe` (via NSIS) and `.msi` (via WiX).
 - Built-in self updater (desktop only)
 - System tray icons
 - Native notifications

+ 82 - 1
core/tauri-config-schema/schema.json

@@ -57,6 +57,11 @@
           "macOS": {
             "minimumSystemVersion": "10.13"
           },
+          "rpm": {
+            "epoch": 0,
+            "files": {},
+            "release": "1"
+          },
           "targets": "all",
           "updater": {
             "active": false,
@@ -205,6 +210,11 @@
             "macOS": {
               "minimumSystemVersion": "10.13"
             },
+            "rpm": {
+              "epoch": 0,
+              "files": {},
+              "release": "1"
+            },
             "targets": "all",
             "updater": {
               "active": false,
@@ -933,7 +943,7 @@
           "type": "boolean"
         },
         "targets": {
-          "description": "The bundle targets, currently supports [\"deb\", \"appimage\", \"nsis\", \"msi\", \"app\", \"dmg\", \"updater\"] or \"all\".",
+          "description": "The bundle targets, currently supports [\"deb\", \"rpm\", \"appimage\", \"nsis\", \"msi\", \"app\", \"dmg\", \"updater\"] or \"all\".",
           "default": "all",
           "allOf": [
             {
@@ -1031,6 +1041,19 @@
             }
           ]
         },
+        "rpm": {
+          "description": "Configuration for the RPM bundle.",
+          "default": {
+            "epoch": 0,
+            "files": {},
+            "release": "1"
+          },
+          "allOf": [
+            {
+              "$ref": "#/definitions/RpmConfig"
+            }
+          ]
+        },
         "dmg": {
           "description": "DMG-specific settings.",
           "default": {
@@ -1170,6 +1193,13 @@
             "deb"
           ]
         },
+        {
+          "description": "The RPM bundle (.rpm).",
+          "type": "string",
+          "enum": [
+            "rpm"
+          ]
+        },
         {
           "description": "The AppImage bundle (.appimage).",
           "type": "string",
@@ -1368,6 +1398,57 @@
       },
       "additionalProperties": false
     },
+    "RpmConfig": {
+      "description": "Configuration for RPM bundles.",
+      "type": "object",
+      "properties": {
+        "license": {
+          "description": "The package's license identifier. If not set, defaults to the license from the Cargo.toml file.",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "depends": {
+          "description": "The list of RPM dependencies your application relies on.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "type": "string"
+          }
+        },
+        "release": {
+          "description": "The RPM release tag.",
+          "default": "1",
+          "type": "string"
+        },
+        "epoch": {
+          "description": "The RPM epoch.",
+          "default": 0,
+          "type": "integer",
+          "format": "uint32",
+          "minimum": 0.0
+        },
+        "files": {
+          "description": "The files to include on the package.",
+          "default": {},
+          "type": "object",
+          "additionalProperties": {
+            "type": "string"
+          }
+        },
+        "desktopTemplate": {
+          "description": "Path to a custom desktop file Handlebars template.\n\nAvailable variables: `categories`, `comment` (optional), `exec`, `icon` and `name`.",
+          "type": [
+            "string",
+            "null"
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
     "DmgConfig": {
       "description": "Configuration for Apple Disk Image (.dmg) bundles.\n\nSee more: https://tauri.app/v1/api/config#dmgconfig",
       "type": "object",

+ 54 - 1
core/tauri-utils/src/config.rs

@@ -78,6 +78,8 @@ impl Default for WindowUrl {
 pub enum BundleType {
   /// The debian bundle (.deb).
   Deb,
+  /// The RPM bundle (.rpm).
+  Rpm,
   /// The AppImage bundle (.appimage).
   AppImage,
   /// The Microsoft Installer bundle (.msi).
@@ -99,6 +101,7 @@ impl Display for BundleType {
       "{}",
       match self {
         Self::Deb => "deb",
+        Self::Rpm => "rpm",
         Self::AppImage => "appimage",
         Self::Msi => "msi",
         Self::Nsis => "nsis",
@@ -127,6 +130,7 @@ impl<'de> Deserialize<'de> for BundleType {
     let s = String::deserialize(deserializer)?;
     match s.to_lowercase().as_str() {
       "deb" => Ok(Self::Deb),
+      "rpm" => Ok(Self::Rpm),
       "appimage" => Ok(Self::AppImage),
       "msi" => Ok(Self::Msi),
       "nsis" => Ok(Self::Nsis),
@@ -282,6 +286,49 @@ pub struct DebConfig {
   pub desktop_template: Option<PathBuf>,
 }
 
+/// Configuration for RPM bundles.
+#[skip_serializing_none]
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+pub struct RpmConfig {
+  /// The package's license identifier. If not set, defaults to the license from
+  /// the Cargo.toml file.
+  pub license: Option<String>,
+  /// The list of RPM dependencies your application relies on.
+  pub depends: Option<Vec<String>>,
+  /// The RPM release tag.
+  #[serde(default = "default_release")]
+  pub release: String,
+  /// The RPM epoch.
+  #[serde(default)]
+  pub epoch: u32,
+  /// The files to include on the package.
+  #[serde(default)]
+  pub files: HashMap<PathBuf, PathBuf>,
+  /// Path to a custom desktop file Handlebars template.
+  ///
+  /// Available variables: `categories`, `comment` (optional), `exec`, `icon` and `name`.
+  pub desktop_template: Option<PathBuf>,
+}
+
+impl Default for RpmConfig {
+  fn default() -> Self {
+    Self {
+      license: None,
+      depends: None,
+      release: default_release(),
+      epoch: 0,
+      files: Default::default(),
+      desktop_template: None,
+    }
+  }
+}
+
+fn default_release() -> String {
+  "1".into()
+}
+
 /// Position coordinates struct.
 #[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
 #[cfg_attr(feature = "schema", derive(JsonSchema))]
@@ -885,7 +932,7 @@ pub struct BundleConfig {
   /// Whether Tauri should bundle your application or just output the executable.
   #[serde(default)]
   pub active: bool,
-  /// The bundle targets, currently supports ["deb", "appimage", "nsis", "msi", "app", "dmg", "updater"] or "all".
+  /// The bundle targets, currently supports ["deb", "rpm", "appimage", "nsis", "msi", "app", "dmg", "updater"] or "all".
   #[serde(default)]
   pub targets: BundleTarget,
   /// The application identifier in reverse domain name notation (e.g. `com.tauri.example`).
@@ -925,6 +972,9 @@ pub struct BundleConfig {
   /// Configuration for the Debian bundle.
   #[serde(default)]
   pub deb: DebConfig,
+  /// Configuration for the RPM bundle.
+  #[serde(default)]
+  pub rpm: RpmConfig,
   /// DMG-specific settings.
   #[serde(default)]
   pub dmg: DmgConfig,
@@ -2518,6 +2568,7 @@ mod build {
       let long_description = quote!(None);
       let appimage = quote!(Default::default());
       let deb = quote!(Default::default());
+      let rpm = quote!(Default::default());
       let dmg = quote!(Default::default());
       let macos = quote!(Default::default());
       let external_bin = opt_vec_str_lit(self.external_bin.as_ref());
@@ -2542,6 +2593,7 @@ mod build {
         long_description,
         appimage,
         deb,
+        rpm,
         dmg,
         macos,
         external_bin,
@@ -2851,6 +2903,7 @@ mod test {
         long_description: None,
         appimage: Default::default(),
         deb: Default::default(),
+        rpm: Default::default(),
         dmg: Default::default(),
         macos: Default::default(),
         external_bin: None,

+ 1 - 0
tooling/bundler/Cargo.toml

@@ -66,6 +66,7 @@ regex = "1"
 heck = "0.4"
 ar = "0.9.0"
 md5 = "0.7.0"
+rpm = "0.13.1"
 
 [lib]
 name = "tauri_bundler"

+ 1 - 1
tooling/bundler/src/bundle.rs

@@ -21,7 +21,7 @@ pub use self::{
   category::AppCategory,
   settings::{
     BundleBinary, BundleSettings, DebianSettings, DmgSettings, MacOsSettings, PackageSettings,
-    PackageType, Position, Settings, SettingsBuilder, Size, UpdaterSettings,
+    PackageType, Position, RpmSettings, Settings, SettingsBuilder, Size, UpdaterSettings,
   },
 };
 #[cfg(target_os = "macos")]

+ 4 - 2
tooling/bundler/src/bundle/category.rs

@@ -142,9 +142,11 @@ impl AppCategory {
     }
   }
 
-  /// Map an AppCategory to the closest set of GNOME desktop registered
+  /// Map an AppCategory to the closest set of Freedesktop registered
   /// categories that matches that category.
-  pub fn gnome_desktop_categories(self) -> &'static str {
+  ///
+  /// Cf https://specifications.freedesktop.org/menu-spec/latest/
+  pub fn freedesktop_categories(self) -> &'static str {
     match &self {
       AppCategory::Business => "Office;",
       AppCategory::DeveloperTool => "Development;",

+ 0 - 1
tooling/bundler/src/bundle/linux/appimage.rs

@@ -31,7 +31,6 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
 
   // generate deb_folder structure
   let (_, icons) = debian::generate_data(settings, &package_dir)?;
-  let icons: Vec<debian::DebIcon> = icons.into_iter().collect();
 
   let output_path = settings.project_out_directory().join("bundle/appimage");
   if output_path.exists() {

+ 7 - 127
tooling/bundler/src/bundle/linux/debian.rs

@@ -23,33 +23,20 @@
 // metadata, as well as generating the md5sums file.  Currently we do not
 // generate postinst or prerm files.
 
-use super::super::common;
+use super::{super::common, freedesktop};
 use crate::Settings;
 use anyhow::Context;
-use handlebars::Handlebars;
 use heck::AsKebabCase;
-use image::{self, codecs::png::PngDecoder, ImageDecoder};
 use libflate::gzip;
 use log::info;
-use serde::Serialize;
 use walkdir::WalkDir;
 
 use std::{
-  collections::BTreeSet,
-  ffi::OsStr,
-  fs::{self, read_to_string, File},
+  fs::{self, File},
   io::{self, Write},
   path::{Path, PathBuf},
 };
 
-#[derive(PartialEq, Eq, PartialOrd, Ord)]
-pub struct DebIcon {
-  pub width: u32,
-  pub height: u32,
-  pub is_high_density: bool,
-  pub path: PathBuf,
-}
-
 /// Bundles the project.
 /// Returns a vector of PathBuf that shows where the DEB was created.
 pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
@@ -112,7 +99,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
 pub fn generate_data(
   settings: &Settings,
   package_dir: &Path,
-) -> crate::Result<(PathBuf, BTreeSet<DebIcon>)> {
+) -> crate::Result<(PathBuf, Vec<freedesktop::Icon>)> {
   // Generate data files.
   let data_dir = package_dir.join("data");
   let bin_dir = data_dir.join("usr/bin");
@@ -129,81 +116,14 @@ pub fn generate_data(
     .copy_binaries(&bin_dir)
     .with_context(|| "Failed to copy external binaries")?;
 
-  let icons =
-    generate_icon_files(settings, &data_dir).with_context(|| "Failed to create icon files")?;
-  generate_desktop_file(settings, &data_dir).with_context(|| "Failed to create desktop file")?;
+  let icons = freedesktop::copy_icon_files(settings, &data_dir)
+    .with_context(|| "Failed to create icon files")?;
+  freedesktop::generate_desktop_file(settings, &settings.deb().desktop_template, &data_dir)
+    .with_context(|| "Failed to create desktop file")?;
 
   Ok((data_dir, icons))
 }
 
-/// Generate the application desktop file and store it under the `data_dir`.
-fn generate_desktop_file(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
-  let bin_name = settings.main_binary_name();
-  let desktop_file_name = format!("{bin_name}.desktop");
-  let desktop_file_path = data_dir
-    .join("usr/share/applications")
-    .join(desktop_file_name);
-
-  // For more information about the format of this file, see
-  // https://developer.gnome.org/integration-guide/stable/desktop-files.html.en
-  let file = &mut common::create_file(&desktop_file_path)?;
-
-  let mut handlebars = Handlebars::new();
-  handlebars.register_escape_fn(handlebars::no_escape);
-  if let Some(template) = &settings.deb().desktop_template {
-    handlebars
-      .register_template_string("main.desktop", read_to_string(template)?)
-      .with_context(|| "Failed to setup custom handlebar template")?;
-  } else {
-    handlebars
-      .register_template_string("main.desktop", include_str!("./templates/main.desktop"))
-      .with_context(|| "Failed to setup custom 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<String>,
-  }
-
-  let mime_type = if let Some(associations) = settings.file_associations() {
-    let mime_types: Vec<&str> = associations
-      .iter()
-      .filter_map(|association| association.mime_type.as_ref())
-      .map(|s| s.as_str())
-      .collect();
-    Some(mime_types.join(";"))
-  } else {
-    None
-  };
-
-  handlebars.render_to_write(
-    "main.desktop",
-    &DesktopTemplateParams {
-      categories: settings
-        .app_category()
-        .map(|app_category| app_category.gnome_desktop_categories())
-        .unwrap_or(""),
-      comment: if !settings.short_description().is_empty() {
-        Some(settings.short_description())
-      } else {
-        None
-      },
-      exec: bin_name,
-      icon: bin_name,
-      name: settings.product_name(),
-      mime_type,
-    },
-    file,
-  )?;
-
-  Ok(())
-}
-
 /// Generates the debian control file and stores it under the `control_dir`.
 fn generate_control_file(
   settings: &Settings,
@@ -309,46 +229,6 @@ fn copy_custom_files(settings: &Settings, data_dir: &Path) -> crate::Result<()>
   Ok(())
 }
 
-/// Generate the icon files and store them under the `data_dir`.
-fn generate_icon_files(settings: &Settings, data_dir: &Path) -> crate::Result<BTreeSet<DebIcon>> {
-  let base_dir = data_dir.join("usr/share/icons/hicolor");
-  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 { "" },
-      settings.main_binary_name()
-    ))
-  };
-  let mut icons = BTreeSet::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 deb_icon = {
-      let decoder = PngDecoder::new(File::open(&icon_path)?)?;
-      let width = decoder.dimensions().0;
-      let height = decoder.dimensions().1;
-      let is_high_density = common::is_retina(&icon_path);
-      let dest_path = get_dest_path(width, height, is_high_density);
-      DebIcon {
-        width,
-        height,
-        is_high_density,
-        path: dest_path,
-      }
-    };
-    if !icons.contains(&deb_icon) {
-      common::copy_file(&icon_path, &deb_icon.path)?;
-      icons.insert(deb_icon);
-    }
-  }
-  Ok(icons)
-}
-
 /// Create an empty file at the given path, creating any parent directories as
 /// needed, then write `data` into the file.
 fn create_file_with_data<P: AsRef<Path>>(path: P, data: &str) -> crate::Result<()> {

+ 160 - 0
tooling/bundler/src/bundle/linux/freedesktop.rs

@@ -0,0 +1,160 @@
+// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
+// Copyright 2019-2023 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::{read_to_string, File};
+use std::path::{Path, PathBuf};
+
+use anyhow::Context;
+use handlebars::Handlebars;
+use image::{self, codecs::png::PngDecoder, ImageDecoder};
+use serde::Serialize;
+
+use crate::bundle::common;
+use crate::Settings;
+
+#[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<BTreeMap<Icon, PathBuf>> {
+  let base_dir = data_dir.join("usr/share/icons/hicolor");
+  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 { "" },
+      settings.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(File::open(&icon_path)?)?;
+      let width = decoder.dimensions().0;
+      let height = decoder.dimensions().1;
+      let is_high_density = common::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<Vec<Icon>> {
+  let icons = self::list_icon_files(settings, data_dir)?;
+  for (icon, src) in &icons {
+    common::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,
+  template_settings: &Option<PathBuf>,
+  data_dir: &Path,
+) -> crate::Result<(PathBuf, PathBuf)> {
+  let bin_name = settings.main_binary_name();
+  let desktop_file_name = format!("{bin_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 common::create_file(&file_path)?;
+
+  let mut handlebars = Handlebars::new();
+  handlebars.register_escape_fn(handlebars::no_escape);
+  if let Some(template) = template_settings {
+    handlebars
+      .register_template_string("main.desktop", read_to_string(template)?)
+      .with_context(|| "Failed to setup custom handlebar template")?;
+  } else {
+    handlebars
+      .register_template_string("main.desktop", include_str!("./templates/main.desktop"))
+      .with_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<String>,
+  }
+
+  let mime_type = if let Some(associations) = settings.file_associations() {
+    let mime_types: Vec<&str> = associations
+      .iter()
+      .filter_map(|association| association.mime_type.as_ref())
+      .map(|s| s.as_str())
+      .collect();
+    Some(mime_types.join(";"))
+  } else {
+    None
+  };
+
+  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,
+      icon: bin_name,
+      name: settings.product_name(),
+      mime_type,
+    },
+    file,
+  )?;
+
+  Ok((file_path, dest_path))
+}

+ 1 - 0
tooling/bundler/src/bundle/linux/mod.rs

@@ -5,4 +5,5 @@
 
 pub mod appimage;
 pub mod debian;
+pub mod freedesktop;
 pub mod rpm;

+ 141 - 4
tooling/bundler/src/bundle/linux/rpm.rs

@@ -5,10 +5,147 @@
 
 use crate::Settings;
 
-use std::path::PathBuf;
+use anyhow::Context;
+use log::info;
+use rpm::{self, signature::pgp, Dependency, FileMode, FileOptions};
+use std::{
+  env,
+  fs::{self, File},
+  path::{Path, PathBuf},
+};
+
+use super::freedesktop;
 
 /// Bundles the project.
-/// Not implemented yet.
-pub fn bundle_project(_settings: &Settings) -> crate::Result<Vec<PathBuf>> {
-  unimplemented!();
+/// Returns a vector of PathBuf that shows where the RPM was created.
+pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
+  let name = settings.main_binary_name();
+  let version = settings.version_string();
+  let release = settings.rpm().release.as_str();
+  let epoch = settings.rpm().epoch;
+  let arch = match settings.binary_arch() {
+    "x86" => "i386",
+    "arm" => "armhfp",
+    other => other,
+  };
+
+  let license = settings
+    .rpm()
+    .license
+    .as_deref()
+    .or_else(|| settings.license())
+    .unwrap_or_default();
+
+  let summary = settings.short_description().trim();
+
+  let package_base_name = format!("{name}-{version}-{release}.{arch}");
+  let package_name = format!("{package_base_name}.rpm");
+
+  let base_dir = settings.project_out_directory().join("bundle/rpm");
+  let package_dir = base_dir.join(&package_base_name);
+  if package_dir.exists() {
+    fs::remove_dir_all(&package_dir)
+      .with_context(|| format!("Failed to remove old {package_base_name}"))?;
+  }
+  fs::create_dir_all(&package_dir)?;
+  let package_path = base_dir.join(&package_name);
+
+  info!(action = "Bundling"; "{} ({})", package_name, package_path.display());
+
+  let mut builder = rpm::PackageBuilder::new(name, version, license, arch, summary)
+    .epoch(epoch)
+    .release(release);
+
+  if let Some(description) = settings.long_description() {
+    builder = builder.description(description.trim())
+  }
+
+  // Add requirements
+  for dep in settings.rpm().depends.as_ref().cloned().unwrap_or_default() {
+    builder = builder.requires(Dependency::any(dep));
+  }
+
+  // Add binaries
+  for bin in settings.binaries() {
+    let src = settings.binary_path(bin);
+    let dest = Path::new("/usr/bin").join(bin.name());
+    builder = builder.with_file(src, FileOptions::new(dest.to_string_lossy()))?;
+  }
+
+  // Add external binaries
+  for src in settings.external_binaries() {
+    let src = src?;
+    let dest = Path::new("/usr/bin").join(
+      src
+        .file_name()
+        .expect("failed to extract external binary filename")
+        .to_string_lossy()
+        .replace(&format!("-{}", settings.target()), ""),
+    );
+    builder = builder.with_file(&src, FileOptions::new(dest.to_string_lossy()))?;
+  }
+
+  // Add resources
+  if settings.resource_files().count() > 0 {
+    let resource_dir = Path::new("/usr/lib").join(settings.main_binary_name());
+    // Create an empty file, needed to add a directory to the RPM package
+    // (cf https://github.com/rpm-rs/rpm/issues/177)
+    let empty_file_path = &package_dir.join("empty");
+    File::create(empty_file_path)?;
+    // Then add the resource directory `/usr/lib/<binary_name>` to the package.
+    builder = builder.with_file(
+      empty_file_path,
+      FileOptions::new(resource_dir.to_string_lossy()).mode(FileMode::Dir { permissions: 0o755 }),
+    )?;
+    // Then add the resources files in that directory
+    for src in settings.resource_files() {
+      let src = src?;
+      let dest = resource_dir.join(tauri_utils::resources::resource_relpath(&src));
+      builder = builder.with_file(&src, FileOptions::new(dest.to_string_lossy()))?;
+    }
+  }
+
+  // Add Desktop entry file
+  let (desktop_src_path, desktop_dest_path) =
+    freedesktop::generate_desktop_file(settings, &settings.rpm().desktop_template, &package_dir)?;
+  builder = builder.with_file(
+    desktop_src_path,
+    FileOptions::new(desktop_dest_path.to_string_lossy()),
+  )?;
+
+  // Add icons
+  for (icon, src) in &freedesktop::list_icon_files(settings, &PathBuf::from("/"))? {
+    builder = builder.with_file(src, FileOptions::new(icon.path.to_string_lossy()))?;
+  }
+
+  // Add custom files
+  for (rpm_path, src_path) in settings.rpm().files.iter() {
+    if src_path.is_file() {
+      builder = builder.with_file(src_path, FileOptions::new(rpm_path.to_string_lossy()))?;
+    } else {
+      for entry in walkdir::WalkDir::new(src_path) {
+        let entry_path = entry?.into_path();
+        if entry_path.is_file() {
+          let dest_path = rpm_path.join(entry_path.strip_prefix(src_path).unwrap());
+          builder =
+            builder.with_file(&entry_path, FileOptions::new(dest_path.to_string_lossy()))?;
+        }
+      }
+    }
+  }
+
+  let pkg = if let Ok(raw_secret_key) = env::var("RPM_SIGN_KEY") {
+    let mut signer = pgp::Signer::load_from_asc(&raw_secret_key)?;
+    if let Ok(passphrase) = env::var("RPM_SIGN_KEY_PASSPHRASE") {
+      signer = signer.with_key_passphrase(passphrase);
+    }
+    builder.build_and_sign(signer)?
+  } else {
+    builder.build()?
+  };
+
+  let mut f = fs::File::create(&package_path)?;
+  pkg.write(&mut f)?;
+
+  Ok(vec![package_path])
 }

+ 41 - 1
tooling/bundler/src/bundle/settings.rs

@@ -44,6 +44,7 @@ impl From<BundleType> for PackageType {
   fn from(bundle: BundleType) -> Self {
     match bundle {
       BundleType::Deb => Self::Deb,
+      BundleType::Rpm => Self::Rpm,
       BundleType::AppImage => Self::AppImage,
       BundleType::Msi => Self::WindowsMsi,
       BundleType::Nsis => Self::Nsis,
@@ -148,6 +149,8 @@ pub struct PackageSettings {
   pub homepage: Option<String>,
   /// the package's authors.
   pub authors: Option<Vec<String>>,
+  /// the package's license.
+  pub license: Option<String>,
   /// the default binary to run.
   pub default_run: Option<String>,
 }
@@ -183,6 +186,31 @@ pub struct DebianSettings {
   pub desktop_template: Option<PathBuf>,
 }
 
+/// The RPM bundle settings.
+#[derive(Clone, Debug, Default)]
+pub struct RpmSettings {
+  /// The name of the package's license.
+  pub license: Option<String>,
+  /// The list of RPM dependencies your application relies on.
+  pub depends: Option<Vec<String>>,
+  /// The RPM release tag.
+  pub release: String,
+  /// The RPM epoch.
+  pub epoch: u32,
+  /// List of custom files to add to the RPM package.
+  /// Maps the path on the RPM package to the path of the file to include (relative to the current working directory).
+  pub files: HashMap<PathBuf, PathBuf>,
+  /// Path to a custom desktop file Handlebars template.
+  ///
+  /// Available variables: `categories`, `comment` (optional), `exec`, `icon` and `name`.
+  ///
+  /// Default file contents:
+  /// ```text
+  #[doc = include_str!("./linux/templates/main.desktop")]
+  /// ```
+  pub desktop_template: Option<PathBuf>,
+}
+
 /// Position coordinates struct.
 #[derive(Clone, Debug, Default)]
 pub struct Position {
@@ -449,6 +477,8 @@ pub struct BundleSettings {
   pub external_bin: Option<Vec<String>>,
   /// Debian-specific settings.
   pub deb: DebianSettings,
+  /// Rpm-specific settings.
+  pub rpm: RpmSettings,
   /// DMG-specific settings.
   pub dmg: DmgSettings,
   /// MacOS-specific settings.
@@ -714,7 +744,7 @@ impl Settings {
     let mut platform_types = match target_os.as_str() {
       "macos" => vec![PackageType::MacOsBundle, PackageType::Dmg],
       "ios" => vec![PackageType::IosBundle],
-      "linux" => vec![PackageType::Deb, PackageType::AppImage],
+      "linux" => vec![PackageType::Deb, PackageType::Rpm, PackageType::AppImage],
       "windows" => vec![PackageType::WindowsMsi, PackageType::Nsis],
       os => {
         return Err(crate::Error::GenericError(format!(
@@ -851,6 +881,11 @@ impl Settings {
     }
   }
 
+  /// Returns the package's license.
+  pub fn license(&self) -> Option<&str> {
+    self.package.license.as_deref()
+  }
+
   /// Returns the package's homepage URL, defaulting to "" if not defined.
   pub fn homepage_url(&self) -> &str {
     self.package.homepage.as_deref().unwrap_or("")
@@ -885,6 +920,11 @@ impl Settings {
     &self.bundle_settings.deb
   }
 
+  /// Returns the RPM settings.
+  pub fn rpm(&self) -> &RpmSettings {
+    &self.bundle_settings.rpm
+  }
+
   /// Returns the DMG settings.
   pub fn dmg(&self) -> &DmgSettings {
     &self.bundle_settings.dmg

+ 4 - 0
tooling/bundler/src/error.rs

@@ -108,6 +108,10 @@ pub enum Error {
   #[cfg(target_os = "macos")]
   #[error(transparent)]
   Plist(#[from] plist::Error),
+  /// Rpm error.
+  #[cfg(target_os = "linux")]
+  #[error("{0}")]
+  RpmError(#[from] rpm::Error),
 }
 
 /// Convenient type alias of Result type.

+ 1 - 1
tooling/bundler/src/lib.rs

@@ -12,7 +12,7 @@
 //! - macOS
 //!   - DMG and App bundles
 //! - Linux
-//!   - Appimage and Debian packages
+//!   - Appimage, Debian and RPM packages
 //! - Windows
 //!   - MSI using WiX
 

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 655 - 7
tooling/cli/Cargo.lock


+ 2 - 0
tooling/cli/ENVIRONMENT_VARIABLES.md

@@ -35,6 +35,8 @@ These environment variables are inputs to the CLI which may have an equivalent C
 - `TAURI_WEBVIEW_AUTOMATION` — Enables webview automation (Linux Only).
 - `TAURI_ANDROID_PROJECT_PATH` — Path of the tauri android project, usually will be `<project>/src-tauri/gen/android`.
 - `TAURI_IOS_PROJECT_PATH` — Path of the tauri iOS project, usually will be `<project>/src-tauri/gen/ios`.
+- `RPM_SIGN_KEY` — The private GPG key used to sign the RPM bundle, exported to its ASCII-armored format.
+- `RPM_SIGN_KEY_PASSPHRASE` — The GPG key passphrase for `RPM_SIGN_KEY`, if needed.
 
 ### Tauri CLI Hook Commands
 

+ 82 - 1
tooling/cli/schema.json

@@ -57,6 +57,11 @@
           "macOS": {
             "minimumSystemVersion": "10.13"
           },
+          "rpm": {
+            "epoch": 0,
+            "files": {},
+            "release": "1"
+          },
           "targets": "all",
           "updater": {
             "active": false,
@@ -205,6 +210,11 @@
             "macOS": {
               "minimumSystemVersion": "10.13"
             },
+            "rpm": {
+              "epoch": 0,
+              "files": {},
+              "release": "1"
+            },
             "targets": "all",
             "updater": {
               "active": false,
@@ -933,7 +943,7 @@
           "type": "boolean"
         },
         "targets": {
-          "description": "The bundle targets, currently supports [\"deb\", \"appimage\", \"nsis\", \"msi\", \"app\", \"dmg\", \"updater\"] or \"all\".",
+          "description": "The bundle targets, currently supports [\"deb\", \"rpm\", \"appimage\", \"nsis\", \"msi\", \"app\", \"dmg\", \"updater\"] or \"all\".",
           "default": "all",
           "allOf": [
             {
@@ -1031,6 +1041,19 @@
             }
           ]
         },
+        "rpm": {
+          "description": "Configuration for the RPM bundle.",
+          "default": {
+            "epoch": 0,
+            "files": {},
+            "release": "1"
+          },
+          "allOf": [
+            {
+              "$ref": "#/definitions/RpmConfig"
+            }
+          ]
+        },
         "dmg": {
           "description": "DMG-specific settings.",
           "default": {
@@ -1170,6 +1193,13 @@
             "deb"
           ]
         },
+        {
+          "description": "The RPM bundle (.rpm).",
+          "type": "string",
+          "enum": [
+            "rpm"
+          ]
+        },
         {
           "description": "The AppImage bundle (.appimage).",
           "type": "string",
@@ -1368,6 +1398,57 @@
       },
       "additionalProperties": false
     },
+    "RpmConfig": {
+      "description": "Configuration for RPM bundles.",
+      "type": "object",
+      "properties": {
+        "license": {
+          "description": "The package's license identifier. If not set, defaults to the license from the Cargo.toml file.",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "depends": {
+          "description": "The list of RPM dependencies your application relies on.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "type": "string"
+          }
+        },
+        "release": {
+          "description": "The RPM release tag.",
+          "default": "1",
+          "type": "string"
+        },
+        "epoch": {
+          "description": "The RPM epoch.",
+          "default": 0,
+          "type": "integer",
+          "format": "uint32",
+          "minimum": 0.0
+        },
+        "files": {
+          "description": "The files to include on the package.",
+          "default": {},
+          "type": "object",
+          "additionalProperties": {
+            "type": "string"
+          }
+        },
+        "desktopTemplate": {
+          "description": "Path to a custom desktop file Handlebars template.\n\nAvailable variables: `categories`, `comment` (optional), `exec`, `icon` and `name`.",
+          "type": [
+            "string",
+            "null"
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
     "DmgConfig": {
       "description": "Configuration for Apple Disk Image (.dmg) bundles.\n\nSee more: https://tauri.app/v1/api/config#dmgconfig",
       "type": "object",

+ 1 - 1
tooling/cli/src/build.rs

@@ -49,7 +49,7 @@ pub struct Options {
   pub features: Option<Vec<String>>,
   /// Space or comma separated list of bundles to package.
   ///
-  /// Each bundle must be one of `deb`, `appimage`, `msi`, `app` or `dmg` on MacOS and `updater` on all platforms.
+  /// Each bundle must be one of `deb`, `rpm`, `appimage`, `msi`, `app` or `dmg` on MacOS and `updater` on all platforms.
   /// If `none` is specified, the bundler will be skipped.
   ///
   /// Note that the `updater` bundle is not automatically added so you must specify it if the updater is enabled.

+ 49 - 10
tooling/cli/src/interface/rust.rs

@@ -23,7 +23,7 @@ use notify_debouncer_mini::new_debouncer;
 use serde::Deserialize;
 use tauri_bundler::{
   AppCategory, BundleBinary, BundleSettings, DebianSettings, DmgSettings, MacOsSettings,
-  PackageSettings, Position, Size, UpdaterSettings, WindowsSettings,
+  PackageSettings, Position, RpmSettings, Size, UpdaterSettings, WindowsSettings,
 };
 use tauri_utils::config::parse::is_configuration_file;
 
@@ -650,6 +650,8 @@ pub struct CargoPackageSettings {
   pub homepage: Option<MaybeWorkspace<String>>,
   /// the package's authors.
   pub authors: Option<MaybeWorkspace<Vec<String>>>,
+  /// the package's license.
+  pub license: Option<String>,
   /// the default binary to run.
   pub default_run: Option<String>,
 }
@@ -704,7 +706,15 @@ impl AppSettings for RustAppSettings {
     config: &Config,
     features: &[String],
   ) -> crate::Result<BundleSettings> {
-    tauri_config_to_bundle_settings(&self.manifest, features, config.tauri.bundle.clone())
+    let arch64bits =
+      self.target_triple.starts_with("x86_64") || self.target_triple.starts_with("aarch64");
+
+    tauri_config_to_bundle_settings(
+      &self.manifest,
+      features,
+      config.tauri.bundle.clone(),
+      arch64bits,
+    )
   }
 
   fn app_binary_path(&self, options: &Options) -> crate::Result<PathBuf> {
@@ -936,6 +946,7 @@ impl RustAppSettings {
           })
           .unwrap()
       }),
+      license: cargo_package_settings.license.clone(),
       default_run: cargo_package_settings.default_run.clone(),
     };
 
@@ -1049,6 +1060,7 @@ fn tauri_config_to_bundle_settings(
   manifest: &Manifest,
   features: &[String],
   config: crate::helpers::config::BundleConfig,
+  arch64bits: bool,
 ) -> crate::Result<BundleSettings> {
   let enabled_features = manifest.all_enabled_features(features);
 
@@ -1069,11 +1081,15 @@ fn tauri_config_to_bundle_settings(
     .resources
     .unwrap_or(BundleResources::List(Vec::new()));
   #[allow(unused_mut)]
-  let mut depends = config.deb.depends.unwrap_or_default();
+  let mut depends_deb = config.deb.depends.unwrap_or_default();
+  #[allow(unused_mut)]
+  let mut depends_rpm = config.rpm.depends.unwrap_or_default();
 
   // set env vars used by the bundler and inject dependencies
   #[cfg(target_os = "linux")]
   {
+    let mut libs: Vec<String> = Vec::new();
+
     if enabled_features.contains(&"tray-icon".into())
       || enabled_features.contains(&"tauri/tray-icon".into())
     {
@@ -1102,19 +1118,30 @@ fn tauri_config_to_bundle_settings(
         .unwrap_or_else(|_| pkgconfig_utils::get_appindicator_library_path());
       match tray_kind {
         pkgconfig_utils::TrayKind::Ayatana => {
-          depends.push("libayatana-appindicator3-1".into());
+          depends_deb.push("libayatana-appindicator3-1".into());
         }
         pkgconfig_utils::TrayKind::Libappindicator => {
-          depends.push("libappindicator3-1".into());
+          depends_deb.push("libappindicator3-1".into());
+          libs.push("libappindicator3.so.1".into());
         }
       }
 
       std::env::set_var("TAURI_TRAY_LIBRARY_PATH", path);
     }
 
-    // provides `libwebkit2gtk-4.1.so.37` and all `4.0` versions have the -37 package name
-    depends.push("libwebkit2gtk-4.1-0".to_string());
-    depends.push("libgtk-3-0".to_string());
+    depends_deb.push("libwebkit2gtk-4.1-0".to_string());
+    depends_deb.push("libgtk-3-0".to_string());
+
+    libs.push("libwebkit2gtk-4.1.so.0".into());
+    libs.push("libgtk-3.so.0".into());
+
+    for lib in libs {
+      let mut requires = lib;
+      if arch64bits {
+        requires.push_str("()(64bit)");
+      }
+      depends_rpm.push(requires);
+    }
   }
 
   #[cfg(windows)]
@@ -1172,14 +1199,26 @@ fn tauri_config_to_bundle_settings(
     long_description: config.long_description,
     external_bin: config.external_bin,
     deb: DebianSettings {
-      depends: if depends.is_empty() {
+      depends: if depends_deb.is_empty() {
         None
       } else {
-        Some(depends)
+        Some(depends_deb)
       },
       files: config.deb.files,
       desktop_template: config.deb.desktop_template,
     },
+    rpm: RpmSettings {
+      license: config.rpm.license,
+      depends: if depends_rpm.is_empty() {
+        None
+      } else {
+        Some(depends_rpm)
+      },
+      release: config.rpm.release,
+      epoch: config.rpm.epoch,
+      files: config.rpm.files,
+      desktop_template: config.rpm.desktop_template,
+    },
     dmg: DmgSettings {
       background: config.dmg.background,
       window_position: config.dmg.window_position.map(|window_position| Position {

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно