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

* feat(bundler): Add RPM packaging

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

* Fix fmt
This commit is contained in:
Olivier Lemasle
2023-12-23 18:38:15 +01:00
committed by GitHub
parent 7e4580afed
commit 091100acbb
20 changed files with 1417 additions and 159 deletions

7
.changes/bundler-rpm.md Normal file
View File

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

View File

@@ -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

View File

@@ -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",

View File

@@ -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,

View File

@@ -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"

View File

@@ -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")]

View File

@@ -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;",

View File

@@ -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() {

View File

@@ -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<()> {

View File

@@ -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))
}

View File

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

View File

@@ -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])
}

View File

@@ -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

View File

@@ -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.

View File

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

785
tooling/cli/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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",

View File

@@ -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.

View File

@@ -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 {