tauri_bundler/bundle/linux/
rpm.rs1use crate::{bundle::settings::Arch, error::ErrorExt, Settings};
7
8use rpm::{self, signature::pgp, Dependency, FileMode, FileOptions};
9use std::{
10 env,
11 fs::{self, File},
12 path::{Path, PathBuf},
13};
14use tauri_utils::config::RpmCompression;
15
16use super::freedesktop;
17
18pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
21 let product_name = settings.product_name();
22 let version = settings.version_string();
23 let release = match settings.rpm().release.as_str() {
24 "" => "1", v => v,
26 };
27 let epoch = settings.rpm().epoch;
28 let arch = match settings.binary_arch() {
29 Arch::X86_64 => "x86_64",
30 Arch::X86 => "i386",
31 Arch::AArch64 => "aarch64",
32 Arch::Armhf => "armhfp",
33 Arch::Armel => "armel",
34 Arch::Riscv64 => "riscv64",
35 target => {
36 return Err(crate::Error::ArchError(format!(
37 "Unsupported architecture: {target:?}"
38 )));
39 }
40 };
41
42 let summary = settings.short_description().trim();
43
44 let package_base_name = format!("{product_name}-{version}-{release}.{arch}");
45 let package_name = format!("{package_base_name}.rpm");
46
47 let base_dir = settings.project_out_directory().join("bundle/rpm");
48 let package_dir = base_dir.join(&package_base_name);
49 if package_dir.exists() {
50 fs::remove_dir_all(&package_dir).fs_context(
51 "Failed to remove old package directory",
52 package_dir.clone(),
53 )?;
54 }
55 fs::create_dir_all(&package_dir)
56 .fs_context("Failed to create package directory", package_dir.clone())?;
57 let package_path = base_dir.join(&package_name);
58
59 log::info!(action = "Bundling"; "{} ({})", package_name, package_path.display());
60
61 let license = settings.license().unwrap_or_default();
62 let name = heck::AsKebabCase(settings.product_name()).to_string();
63
64 let compression = settings
65 .rpm()
66 .compression
67 .map(|c| match c {
68 RpmCompression::Gzip { level } => rpm::CompressionWithLevel::Gzip(level),
69 RpmCompression::Zstd { level } => rpm::CompressionWithLevel::Zstd(level),
70 RpmCompression::Xz { level } => rpm::CompressionWithLevel::Xz(level),
71 RpmCompression::Bzip2 { level } => rpm::CompressionWithLevel::Bzip2(level),
72 _ => rpm::CompressionWithLevel::None,
73 })
74 .unwrap_or(rpm::CompressionWithLevel::Gzip(6));
77
78 let mut builder = rpm::PackageBuilder::new(&name, version, &license, arch, summary)
79 .epoch(epoch)
80 .release(release)
81 .compression(compression);
82
83 if let Some(description) = settings.long_description() {
84 builder = builder.description(description);
85 }
86
87 if let Some(homepage) = settings.homepage_url() {
88 builder = builder.url(homepage);
89 }
90
91 for dep in settings.rpm().depends.as_ref().cloned().unwrap_or_default() {
93 builder = builder.requires(Dependency::any(dep));
94 }
95
96 for dep in settings
98 .rpm()
99 .provides
100 .as_ref()
101 .cloned()
102 .unwrap_or_default()
103 {
104 builder = builder.provides(Dependency::any(dep));
105 }
106
107 for dep in settings
109 .rpm()
110 .recommends
111 .as_ref()
112 .cloned()
113 .unwrap_or_default()
114 {
115 builder = builder.recommends(Dependency::any(dep));
116 }
117
118 for dep in settings
120 .rpm()
121 .conflicts
122 .as_ref()
123 .cloned()
124 .unwrap_or_default()
125 {
126 builder = builder.conflicts(Dependency::any(dep));
127 }
128
129 for dep in settings
131 .rpm()
132 .obsoletes
133 .as_ref()
134 .cloned()
135 .unwrap_or_default()
136 {
137 builder = builder.obsoletes(Dependency::any(dep));
138 }
139
140 for bin in settings.binaries() {
142 let src = settings.binary_path(bin);
143 let dest = Path::new("/usr/bin").join(bin.name());
144 builder = builder.with_file(src, FileOptions::new(dest.to_string_lossy()))?;
145 }
146
147 for src in settings.external_binaries() {
149 let src = src?;
150 let dest = Path::new("/usr/bin").join(
151 src
152 .file_name()
153 .expect("failed to extract external binary filename")
154 .to_string_lossy()
155 .replace(&format!("-{}", settings.target()), ""),
156 );
157 builder = builder.with_file(&src, FileOptions::new(dest.to_string_lossy()))?;
158 }
159
160 if let Some(script_path) = &settings.rpm().pre_install_script {
162 let script = fs::read_to_string(script_path)?;
163 builder = builder.pre_install_script(script);
164 }
165
166 if let Some(script_path) = &settings.rpm().post_install_script {
167 let script = fs::read_to_string(script_path)?;
168 builder = builder.post_install_script(script);
169 }
170
171 if let Some(script_path) = &settings.rpm().pre_remove_script {
172 let script = fs::read_to_string(script_path)?;
173 builder = builder.pre_uninstall_script(script);
174 }
175
176 if let Some(script_path) = &settings.rpm().post_remove_script {
177 let script = fs::read_to_string(script_path)?;
178 builder = builder.post_uninstall_script(script);
179 }
180
181 if settings.resource_files().count() > 0 {
183 let resource_dir = Path::new("/usr/lib").join(settings.product_name());
184 let empty_file_path = &package_dir.join("empty");
187 File::create(empty_file_path)?;
188 builder = builder.with_file(
190 empty_file_path,
191 FileOptions::new(resource_dir.to_string_lossy()).mode(FileMode::Dir { permissions: 0o755 }),
192 )?;
193 for resource in settings.resource_files().iter() {
195 let resource = resource?;
196 let dest = resource_dir.join(resource.target());
197 builder = builder.with_file(resource.path(), FileOptions::new(dest.to_string_lossy()))?;
198 }
199 }
200
201 let (desktop_src_path, desktop_dest_path) =
203 freedesktop::generate_desktop_file(settings, &settings.rpm().desktop_template, &package_dir)?;
204 builder = builder.with_file(
205 desktop_src_path,
206 FileOptions::new(desktop_dest_path.to_string_lossy()),
207 )?;
208
209 for (icon, src) in &freedesktop::list_icon_files(settings, &PathBuf::from("/"))? {
211 builder = builder.with_file(src, FileOptions::new(icon.path.to_string_lossy()))?;
212 }
213
214 for (rpm_path, src_path) in settings.rpm().files.iter() {
216 if src_path.is_file() {
217 builder = builder.with_file(src_path, FileOptions::new(rpm_path.to_string_lossy()))?;
218 } else {
219 for entry in walkdir::WalkDir::new(src_path) {
220 let entry_path = entry?.into_path();
221 if entry_path.is_file() {
222 let dest_path = rpm_path.join(entry_path.strip_prefix(src_path).unwrap());
223 builder =
224 builder.with_file(&entry_path, FileOptions::new(dest_path.to_string_lossy()))?;
225 }
226 }
227 }
228 }
229
230 let pkg = if let Ok(raw_secret_key) = env::var("TAURI_SIGNING_RPM_KEY") {
231 let mut signer = pgp::Signer::load_from_asc(&raw_secret_key)?;
232 if let Ok(passphrase) = env::var("TAURI_SIGNING_RPM_KEY_PASSPHRASE") {
233 signer = signer.with_key_passphrase(passphrase);
234 }
235 builder.build_and_sign(signer)?
236 } else {
237 builder.build()?
238 };
239
240 let mut f = fs::File::create(&package_path)?;
241 pkg.write(&mut f)?;
242 Ok(vec![package_path])
243}