tauri_bundler/bundle/linux/
rpm.rs

1// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
2// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
3// SPDX-License-Identifier: Apache-2.0
4// SPDX-License-Identifier: MIT
5
6use 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
18/// Bundles the project.
19/// Returns a vector of PathBuf that shows where the RPM was created.
20pub 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", // Considered the default. If left empty, you get file with "-.".
25    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    // This matches .deb compression. On a 240MB source binary the bundle will be 100KB larger than rpm's default while reducing build times by ~25%.
75    // TODO: Default to Zstd in v3 to match rpm-rs new default in 0.16
76    .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  // Add requirements
92  for dep in settings.rpm().depends.as_ref().cloned().unwrap_or_default() {
93    builder = builder.requires(Dependency::any(dep));
94  }
95
96  // Add provides
97  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  // Add recommends
108  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  // Add conflicts
119  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  // Add obsoletes
130  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  // Add binaries
141  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  // Add external binaries
148  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  // Add scripts
161  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  // Add resources
182  if settings.resource_files().count() > 0 {
183    let resource_dir = Path::new("/usr/lib").join(settings.product_name());
184    // Create an empty file, needed to add a directory to the RPM package
185    // (cf https://github.com/rpm-rs/rpm/issues/177)
186    let empty_file_path = &package_dir.join("empty");
187    File::create(empty_file_path)?;
188    // Then add the resource directory `/usr/lib/<product_name>` to the package.
189    builder = builder.with_file(
190      empty_file_path,
191      FileOptions::new(resource_dir.to_string_lossy()).mode(FileMode::Dir { permissions: 0o755 }),
192    )?;
193    // Then add the resources files in that directory
194    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  // Add Desktop entry file
202  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  // Add icons
210  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  // Add custom files
215  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}