tauri_bundler/bundle/linux/
debian.rs1use super::freedesktop;
27use crate::{
28 bundle::settings::Arch,
29 error::{Context, ErrorExt},
30 utils::fs_utils,
31 Settings,
32};
33use flate2::{write::GzEncoder, Compression};
34use tar::HeaderMode;
35use walkdir::WalkDir;
36
37use std::{
38 fs::{self, File, OpenOptions},
39 io::{self, Write},
40 os::unix::fs::{MetadataExt, OpenOptionsExt},
41 path::{Path, PathBuf},
42};
43
44pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
47 let arch = match settings.binary_arch() {
48 Arch::X86_64 => "amd64",
49 Arch::X86 => "i386",
50 Arch::AArch64 => "arm64",
51 Arch::Armhf => "armhf",
52 Arch::Armel => "armel",
53 Arch::Riscv64 => "riscv64",
54 target => {
55 return Err(crate::Error::ArchError(format!(
56 "Unsupported architecture: {target:?}"
57 )));
58 }
59 };
60 let package_base_name = format!(
61 "{}_{}_{}",
62 settings.product_name(),
63 settings.version_string(),
64 arch
65 );
66 let package_name = format!("{package_base_name}.deb");
67
68 let base_dir = settings.project_out_directory().join("bundle/deb");
69 let package_dir = base_dir.join(&package_base_name);
70 if package_dir.exists() {
71 fs::remove_dir_all(&package_dir).fs_context(
72 "Failed to Remove old package directory",
73 package_dir.clone(),
74 )?;
75 }
76 let package_path = base_dir.join(&package_name);
77
78 log::info!(action = "Bundling"; "{} ({})", package_name, package_path.display());
79
80 let (data_dir, _) =
81 generate_data(settings, &package_dir).context("Failed to build data folders and files")?;
82 fs_utils::copy_custom_files(&settings.deb().files, &data_dir)
83 .context("Failed to copy custom files")?;
84
85 let control_dir = package_dir.join("control");
87 generate_control_file(settings, arch, &control_dir, &data_dir)
88 .context("Failed to create control file")?;
89 generate_scripts(settings, &control_dir).context("Failed to create control scripts")?;
90 generate_md5sums(&control_dir, &data_dir).context("Failed to create md5sums file")?;
91
92 let debian_binary_path = package_dir.join("debian-binary");
95 create_file_with_data(&debian_binary_path, "2.0\n")
96 .context("Failed to create debian-binary file")?;
97
98 let control_tar_gz_path =
100 tar_and_gzip_dir(control_dir).with_context(|| "Failed to tar/gzip control directory")?;
101 let data_tar_gz_path =
102 tar_and_gzip_dir(data_dir).with_context(|| "Failed to tar/gzip data directory")?;
103 create_archive(
104 vec![debian_binary_path, control_tar_gz_path, data_tar_gz_path],
105 &package_path,
106 )
107 .with_context(|| "Failed to create package archive")?;
108 Ok(vec![package_path])
109}
110
111pub fn generate_data(
113 settings: &Settings,
114 package_dir: &Path,
115) -> crate::Result<(PathBuf, Vec<freedesktop::Icon>)> {
116 let data_dir = package_dir.join("data");
118 let bin_dir = data_dir.join("usr/bin");
119
120 for bin in settings.binaries() {
121 let bin_path = settings.binary_path(bin);
122 fs_utils::copy_file(&bin_path, &bin_dir.join(bin.name()))
123 .with_context(|| format!("Failed to copy binary from {bin_path:?}"))?;
124 }
125
126 copy_resource_files(settings, &data_dir).with_context(|| "Failed to copy resource files")?;
127
128 settings
129 .copy_binaries(&bin_dir)
130 .with_context(|| "Failed to copy external binaries")?;
131
132 let icons = freedesktop::copy_icon_files(settings, &data_dir)
133 .with_context(|| "Failed to create icon files")?;
134 freedesktop::generate_desktop_file(settings, &settings.deb().desktop_template, &data_dir)
135 .with_context(|| "Failed to create desktop file")?;
136 generate_changelog_file(settings, &data_dir)
137 .with_context(|| "Failed to create changelog.gz file")?;
138
139 Ok((data_dir, icons))
140}
141
142fn generate_changelog_file(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
145 if let Some(changelog_src_path) = &settings.deb().changelog {
146 let mut src_file = File::open(changelog_src_path)?;
147 let product_name = settings.product_name();
148 let dest_path = data_dir.join(format!("usr/share/doc/{product_name}/changelog.gz"));
149
150 let changelog_file = fs_utils::create_file(&dest_path)?;
151 let mut gzip_encoder = GzEncoder::new(changelog_file, Compression::new(9));
152 io::copy(&mut src_file, &mut gzip_encoder)?;
153
154 let mut changelog_file = gzip_encoder.finish()?;
155 changelog_file.flush()?;
156 }
157 Ok(())
158}
159
160fn generate_control_file(
162 settings: &Settings,
163 arch: &str,
164 control_dir: &Path,
165 data_dir: &Path,
166) -> crate::Result<()> {
167 let dest_path = control_dir.join("control");
170 let mut file = fs_utils::create_file(&dest_path)?;
171 let package = heck::AsKebabCase(settings.product_name());
172 writeln!(file, "Package: {package}")?;
173 writeln!(file, "Version: {}", settings.version_string())?;
174 writeln!(file, "Architecture: {arch}")?;
175 writeln!(file, "Installed-Size: {}", total_dir_size(data_dir)? / 1024)?;
177 let authors = settings
178 .authors_comma_separated()
179 .or_else(|| settings.publisher().map(ToString::to_string))
180 .unwrap_or_else(|| {
181 settings
182 .bundle_identifier()
183 .split('.')
184 .nth(1)
185 .unwrap_or(settings.bundle_identifier())
186 .to_string()
187 });
188
189 writeln!(file, "Maintainer: {authors}")?;
190 if let Some(section) = &settings.deb().section {
191 writeln!(file, "Section: {section}")?;
192 }
193 if let Some(priority) = &settings.deb().priority {
194 writeln!(file, "Priority: {priority}")?;
195 } else {
196 writeln!(file, "Priority: optional")?;
197 }
198
199 if let Some(homepage) = settings.homepage_url() {
200 writeln!(file, "Homepage: {homepage}")?;
201 }
202
203 let dependencies = settings.deb().depends.as_ref().cloned().unwrap_or_default();
204 if !dependencies.is_empty() {
205 writeln!(file, "Depends: {}", dependencies.join(", "))?;
206 }
207 let dependencies = settings
208 .deb()
209 .recommends
210 .as_ref()
211 .cloned()
212 .unwrap_or_default();
213 if !dependencies.is_empty() {
214 writeln!(file, "Recommends: {}", dependencies.join(", "))?;
215 }
216 let provides = settings
217 .deb()
218 .provides
219 .as_ref()
220 .cloned()
221 .unwrap_or_default();
222 if !provides.is_empty() {
223 writeln!(file, "Provides: {}", provides.join(", "))?;
224 }
225 let conflicts = settings
226 .deb()
227 .conflicts
228 .as_ref()
229 .cloned()
230 .unwrap_or_default();
231 if !conflicts.is_empty() {
232 writeln!(file, "Conflicts: {}", conflicts.join(", "))?;
233 }
234 let replaces = settings
235 .deb()
236 .replaces
237 .as_ref()
238 .cloned()
239 .unwrap_or_default();
240 if !replaces.is_empty() {
241 writeln!(file, "Replaces: {}", replaces.join(", "))?;
242 }
243 let mut short_description = settings.short_description().trim();
244 if short_description.is_empty() {
245 short_description = "(none)";
246 }
247 let mut long_description = settings.long_description().unwrap_or("").trim();
248 if long_description.is_empty() {
249 long_description = "(none)";
250 }
251 writeln!(file, "Description: {short_description}")?;
252 for line in long_description.lines() {
253 let line = line.trim();
254 if line.is_empty() {
255 writeln!(file, " .")?;
256 } else {
257 writeln!(file, " {line}")?;
258 }
259 }
260 file.flush()?;
261 Ok(())
262}
263
264fn generate_scripts(settings: &Settings, control_dir: &Path) -> crate::Result<()> {
265 if let Some(script_path) = &settings.deb().pre_install_script {
266 let dest_path = control_dir.join("preinst");
267 create_script_file_from_path(script_path, &dest_path)?
268 }
269
270 if let Some(script_path) = &settings.deb().post_install_script {
271 let dest_path = control_dir.join("postinst");
272 create_script_file_from_path(script_path, &dest_path)?
273 }
274
275 if let Some(script_path) = &settings.deb().pre_remove_script {
276 let dest_path = control_dir.join("prerm");
277 create_script_file_from_path(script_path, &dest_path)?
278 }
279
280 if let Some(script_path) = &settings.deb().post_remove_script {
281 let dest_path = control_dir.join("postrm");
282 create_script_file_from_path(script_path, &dest_path)?
283 }
284 Ok(())
285}
286
287fn create_script_file_from_path(from: &PathBuf, to: &PathBuf) -> crate::Result<()> {
288 let mut from = File::open(from)?;
289 let mut file = OpenOptions::new()
290 .create(true)
291 .truncate(true)
292 .write(true)
293 .mode(0o755)
294 .open(to)?;
295 std::io::copy(&mut from, &mut file)?;
296 Ok(())
297}
298
299fn generate_md5sums(control_dir: &Path, data_dir: &Path) -> crate::Result<()> {
302 let md5sums_path = control_dir.join("md5sums");
303 let mut md5sums_file = fs_utils::create_file(&md5sums_path)?;
304 for entry in WalkDir::new(data_dir) {
305 let entry = entry?;
306 let path = entry.path();
307 if path.is_dir() {
308 continue;
309 }
310 let mut file = File::open(path)?;
311 let mut hash = md5::Context::new();
312 io::copy(&mut file, &mut hash)?;
313 for byte in hash.finalize().iter() {
314 write!(md5sums_file, "{byte:02x}")?;
315 }
316 let rel_path = path.strip_prefix(data_dir)?;
317 let path_str = rel_path.to_str().ok_or_else(|| {
318 let msg = format!("Non-UTF-8 path: {rel_path:?}");
319 io::Error::new(io::ErrorKind::InvalidData, msg)
320 })?;
321 writeln!(md5sums_file, " {path_str}")?;
322 }
323 Ok(())
324}
325
326fn copy_resource_files(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
329 let resource_dir = data_dir.join("usr/lib").join(settings.product_name());
330 settings.copy_resources(&resource_dir)
331}
332
333fn create_file_with_data<P: AsRef<Path>>(path: P, data: &str) -> crate::Result<()> {
336 let mut file = fs_utils::create_file(path.as_ref())?;
337 file.write_all(data.as_bytes())?;
338 file.flush()?;
339 Ok(())
340}
341
342fn total_dir_size(dir: &Path) -> crate::Result<u64> {
345 let mut total: u64 = 0;
346 for entry in WalkDir::new(dir) {
347 total += entry?.metadata()?.len();
348 }
349 Ok(total)
350}
351
352fn create_tar_from_dir<P: AsRef<Path>, W: Write>(src_dir: P, dest_file: W) -> crate::Result<W> {
354 let src_dir = src_dir.as_ref();
355 let mut tar_builder = tar::Builder::new(dest_file);
356 for entry in WalkDir::new(src_dir) {
357 let entry = entry?;
358 let src_path = entry.path();
359 if src_path == src_dir {
360 continue;
361 }
362 let dest_path = src_path.strip_prefix(src_dir)?;
363 let stat = fs::metadata(src_path)?;
364 let mut header = tar::Header::new_gnu();
365 header.set_metadata_in_mode(&stat, HeaderMode::Deterministic);
366 header.set_mtime(stat.mtime() as u64);
367
368 if entry.file_type().is_dir() {
369 tar_builder.append_data(&mut header, dest_path, &mut io::empty())?;
370 } else {
371 let mut src_file = fs::File::open(src_path)?;
372 tar_builder.append_data(&mut header, dest_path, &mut src_file)?;
373 }
374 }
375 let dest_file = tar_builder.into_inner()?;
376 Ok(dest_file)
377}
378
379fn tar_and_gzip_dir<P: AsRef<Path>>(src_dir: P) -> crate::Result<PathBuf> {
383 let src_dir = src_dir.as_ref();
384 let dest_path = src_dir.with_extension("tar.gz");
385 let dest_file = fs_utils::create_file(&dest_path)?;
386 let gzip_encoder = GzEncoder::new(dest_file, Compression::default());
387 let gzip_encoder = create_tar_from_dir(src_dir, gzip_encoder)?;
388 let mut dest_file = gzip_encoder.finish()?;
389 dest_file.flush()?;
390 Ok(dest_path)
391}
392
393fn create_archive(srcs: Vec<PathBuf>, dest: &Path) -> crate::Result<()> {
396 let mut builder = ar::Builder::new(fs_utils::create_file(dest)?);
397 for path in &srcs {
398 builder.append_path(path)?;
399 }
400 builder.into_inner()?.flush()?;
401 Ok(())
402}