debian.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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. // The structure of a Debian package looks something like this:
  6. //
  7. // foobar_1.2.3_i386.deb # Actually an ar archive
  8. // debian-binary # Specifies deb format version (2.0 in our case)
  9. // control.tar.gz # Contains files controlling the installation:
  10. // control # Basic package metadata
  11. // md5sums # Checksums for files in data.tar.gz below
  12. // postinst # Post-installation script (optional)
  13. // prerm # Pre-uninstallation script (optional)
  14. // data.tar.gz # Contains files to be installed:
  15. // usr/bin/foobar # Binary executable file
  16. // usr/share/applications/foobar.desktop # Desktop file (for apps)
  17. // usr/share/icons/hicolor/... # Icon files (for apps)
  18. // usr/lib/foobar/... # Other resource files
  19. //
  20. // For cargo-bundle, we put bundle resource files under /usr/lib/package_name/,
  21. // and then generate the desktop file and control file from the bundle
  22. // metadata, as well as generating the md5sums file. Currently we do not
  23. // generate postinst or prerm files.
  24. use super::{super::common, freedesktop};
  25. use crate::Settings;
  26. use anyhow::Context;
  27. use flate2::{write::GzEncoder, Compression};
  28. use tar::HeaderMode;
  29. use walkdir::WalkDir;
  30. use std::{
  31. fs::{self, File, OpenOptions},
  32. io::{self, Write},
  33. os::unix::fs::{MetadataExt, OpenOptionsExt},
  34. path::{Path, PathBuf},
  35. };
  36. /// Bundles the project.
  37. /// Returns a vector of PathBuf that shows where the DEB was created.
  38. pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
  39. let arch = match settings.binary_arch() {
  40. "x86" => "i386",
  41. "x86_64" => "amd64",
  42. // ARM64 is detected differently, armel isn't supported, so armhf is the only reasonable choice here.
  43. "arm" => "armhf",
  44. "aarch64" => "arm64",
  45. other => other,
  46. };
  47. let package_base_name = format!(
  48. "{}_{}_{}",
  49. settings.product_name(),
  50. settings.version_string(),
  51. arch
  52. );
  53. let package_name = format!("{package_base_name}.deb");
  54. let base_dir = settings.project_out_directory().join("bundle/deb");
  55. let package_dir = base_dir.join(&package_base_name);
  56. if package_dir.exists() {
  57. fs::remove_dir_all(&package_dir)
  58. .with_context(|| format!("Failed to remove old {package_base_name}"))?;
  59. }
  60. let package_path = base_dir.join(&package_name);
  61. log::info!(action = "Bundling"; "{} ({})", package_name, package_path.display());
  62. let (data_dir, _) = generate_data(settings, &package_dir)
  63. .with_context(|| "Failed to build data folders and files")?;
  64. common::copy_custom_files(&settings.deb().files, &data_dir)
  65. .with_context(|| "Failed to copy custom files")?;
  66. // Generate control files.
  67. let control_dir = package_dir.join("control");
  68. generate_control_file(settings, arch, &control_dir, &data_dir)
  69. .with_context(|| "Failed to create control file")?;
  70. generate_scripts(settings, &control_dir).with_context(|| "Failed to create control scripts")?;
  71. generate_md5sums(&control_dir, &data_dir).with_context(|| "Failed to create md5sums file")?;
  72. // Generate `debian-binary` file; see
  73. // http://www.tldp.org/HOWTO/Debian-Binary-Package-Building-HOWTO/x60.html#AEN66
  74. let debian_binary_path = package_dir.join("debian-binary");
  75. create_file_with_data(&debian_binary_path, "2.0\n")
  76. .with_context(|| "Failed to create debian-binary file")?;
  77. // Apply tar/gzip/ar to create the final package file.
  78. let control_tar_gz_path =
  79. tar_and_gzip_dir(control_dir).with_context(|| "Failed to tar/gzip control directory")?;
  80. let data_tar_gz_path =
  81. tar_and_gzip_dir(data_dir).with_context(|| "Failed to tar/gzip data directory")?;
  82. create_archive(
  83. vec![debian_binary_path, control_tar_gz_path, data_tar_gz_path],
  84. &package_path,
  85. )
  86. .with_context(|| "Failed to create package archive")?;
  87. Ok(vec![package_path])
  88. }
  89. /// Generate the debian data folders and files.
  90. pub fn generate_data(
  91. settings: &Settings,
  92. package_dir: &Path,
  93. ) -> crate::Result<(PathBuf, Vec<freedesktop::Icon>)> {
  94. // Generate data files.
  95. let data_dir = package_dir.join("data");
  96. let bin_dir = data_dir.join("usr/bin");
  97. for bin in settings.binaries() {
  98. let bin_path = settings.binary_path(bin);
  99. common::copy_file(&bin_path, bin_dir.join(bin.name()))
  100. .with_context(|| format!("Failed to copy binary from {bin_path:?}"))?;
  101. }
  102. copy_resource_files(settings, &data_dir).with_context(|| "Failed to copy resource files")?;
  103. settings
  104. .copy_binaries(&bin_dir)
  105. .with_context(|| "Failed to copy external binaries")?;
  106. let icons = freedesktop::copy_icon_files(settings, &data_dir)
  107. .with_context(|| "Failed to create icon files")?;
  108. freedesktop::generate_desktop_file(settings, &settings.deb().desktop_template, &data_dir)
  109. .with_context(|| "Failed to create desktop file")?;
  110. generate_changelog_file(settings, &data_dir)
  111. .with_context(|| "Failed to create changelog.gz file")?;
  112. Ok((data_dir, icons))
  113. }
  114. /// Generate the Changelog file by compressing, to be stored at /usr/share/doc/package-name/changelog.gz. See
  115. /// <https://www.debian.org/doc/debian-policy/ch-docs.html#changelog-files-and-release-notes>
  116. fn generate_changelog_file(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
  117. if let Some(changelog_src_path) = &settings.deb().changelog {
  118. let mut src_file = File::open(changelog_src_path)?;
  119. let bin_name = settings.main_binary_name();
  120. let dest_path = data_dir.join(format!("usr/share/doc/{}/changelog.gz", bin_name));
  121. let changelog_file = common::create_file(&dest_path)?;
  122. let mut gzip_encoder = GzEncoder::new(changelog_file, Compression::new(9));
  123. io::copy(&mut src_file, &mut gzip_encoder)?;
  124. let mut changelog_file = gzip_encoder.finish()?;
  125. changelog_file.flush()?;
  126. }
  127. Ok(())
  128. }
  129. /// Generates the debian control file and stores it under the `control_dir`.
  130. fn generate_control_file(
  131. settings: &Settings,
  132. arch: &str,
  133. control_dir: &Path,
  134. data_dir: &Path,
  135. ) -> crate::Result<()> {
  136. // For more information about the format of this file, see
  137. // https://www.debian.org/doc/debian-policy/ch-controlfields.html
  138. let dest_path = control_dir.join("control");
  139. let mut file = common::create_file(&dest_path)?;
  140. let package = heck::AsKebabCase(settings.product_name());
  141. writeln!(file, "Package: {}", package)?;
  142. writeln!(file, "Version: {}", settings.version_string())?;
  143. writeln!(file, "Architecture: {arch}")?;
  144. // Installed-Size must be divided by 1024, see https://www.debian.org/doc/debian-policy/ch-controlfields.html#installed-size
  145. writeln!(file, "Installed-Size: {}", total_dir_size(data_dir)? / 1024)?;
  146. let authors = settings.authors_comma_separated().unwrap_or_default();
  147. writeln!(file, "Maintainer: {authors}")?;
  148. if let Some(section) = &settings.deb().section {
  149. writeln!(file, "Section: {}", section)?;
  150. }
  151. if let Some(priority) = &settings.deb().priority {
  152. writeln!(file, "Priority: {}", priority)?;
  153. } else {
  154. writeln!(file, "Priority: optional")?;
  155. }
  156. if let Some(homepage) = settings.homepage_url() {
  157. writeln!(file, "Homepage: {}", homepage)?;
  158. }
  159. let dependencies = settings.deb().depends.as_ref().cloned().unwrap_or_default();
  160. if !dependencies.is_empty() {
  161. writeln!(file, "Depends: {}", dependencies.join(", "))?;
  162. }
  163. let provides = settings
  164. .deb()
  165. .provides
  166. .as_ref()
  167. .cloned()
  168. .unwrap_or_default();
  169. if !provides.is_empty() {
  170. writeln!(file, "Provides: {}", provides.join(", "))?;
  171. }
  172. let conflicts = settings
  173. .deb()
  174. .conflicts
  175. .as_ref()
  176. .cloned()
  177. .unwrap_or_default();
  178. if !conflicts.is_empty() {
  179. writeln!(file, "Conflicts: {}", conflicts.join(", "))?;
  180. }
  181. let replaces = settings
  182. .deb()
  183. .replaces
  184. .as_ref()
  185. .cloned()
  186. .unwrap_or_default();
  187. if !replaces.is_empty() {
  188. writeln!(file, "Replaces: {}", replaces.join(", "))?;
  189. }
  190. let mut short_description = settings.short_description().trim();
  191. if short_description.is_empty() {
  192. short_description = "(none)";
  193. }
  194. let mut long_description = settings.long_description().unwrap_or("").trim();
  195. if long_description.is_empty() {
  196. long_description = "(none)";
  197. }
  198. writeln!(file, "Description: {short_description}")?;
  199. for line in long_description.lines() {
  200. let line = line.trim();
  201. if line.is_empty() {
  202. writeln!(file, " .")?;
  203. } else {
  204. writeln!(file, " {line}")?;
  205. }
  206. }
  207. file.flush()?;
  208. Ok(())
  209. }
  210. fn generate_scripts(settings: &Settings, control_dir: &Path) -> crate::Result<()> {
  211. if let Some(script_path) = &settings.deb().pre_install_script {
  212. let dest_path = control_dir.join("preinst");
  213. create_script_file_from_path(script_path, &dest_path)?
  214. }
  215. if let Some(script_path) = &settings.deb().post_install_script {
  216. let dest_path = control_dir.join("postinst");
  217. create_script_file_from_path(script_path, &dest_path)?
  218. }
  219. if let Some(script_path) = &settings.deb().pre_remove_script {
  220. let dest_path = control_dir.join("prerm");
  221. create_script_file_from_path(script_path, &dest_path)?
  222. }
  223. if let Some(script_path) = &settings.deb().post_remove_script {
  224. let dest_path = control_dir.join("postrm");
  225. create_script_file_from_path(script_path, &dest_path)?
  226. }
  227. Ok(())
  228. }
  229. fn create_script_file_from_path(from: &PathBuf, to: &PathBuf) -> crate::Result<()> {
  230. let mut from = File::open(from)?;
  231. let mut file = OpenOptions::new()
  232. .create(true)
  233. .truncate(true)
  234. .write(true)
  235. .mode(0o755)
  236. .open(to)?;
  237. std::io::copy(&mut from, &mut file)?;
  238. Ok(())
  239. }
  240. /// Create an `md5sums` file in the `control_dir` containing the MD5 checksums
  241. /// for each file within the `data_dir`.
  242. fn generate_md5sums(control_dir: &Path, data_dir: &Path) -> crate::Result<()> {
  243. let md5sums_path = control_dir.join("md5sums");
  244. let mut md5sums_file = common::create_file(&md5sums_path)?;
  245. for entry in WalkDir::new(data_dir) {
  246. let entry = entry?;
  247. let path = entry.path();
  248. if path.is_dir() {
  249. continue;
  250. }
  251. let mut file = File::open(path)?;
  252. let mut hash = md5::Context::new();
  253. io::copy(&mut file, &mut hash)?;
  254. for byte in hash.compute().iter() {
  255. write!(md5sums_file, "{byte:02x}")?;
  256. }
  257. let rel_path = path.strip_prefix(data_dir)?;
  258. let path_str = rel_path.to_str().ok_or_else(|| {
  259. let msg = format!("Non-UTF-8 path: {rel_path:?}");
  260. io::Error::new(io::ErrorKind::InvalidData, msg)
  261. })?;
  262. writeln!(md5sums_file, " {path_str}")?;
  263. }
  264. Ok(())
  265. }
  266. /// Copy the bundle's resource files into an appropriate directory under the
  267. /// `data_dir`.
  268. fn copy_resource_files(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
  269. let resource_dir = data_dir.join("usr/lib").join(settings.main_binary_name());
  270. settings.copy_resources(&resource_dir)
  271. }
  272. /// Create an empty file at the given path, creating any parent directories as
  273. /// needed, then write `data` into the file.
  274. fn create_file_with_data<P: AsRef<Path>>(path: P, data: &str) -> crate::Result<()> {
  275. let mut file = common::create_file(path.as_ref())?;
  276. file.write_all(data.as_bytes())?;
  277. file.flush()?;
  278. Ok(())
  279. }
  280. /// Computes the total size, in bytes, of the given directory and all of its
  281. /// contents.
  282. fn total_dir_size(dir: &Path) -> crate::Result<u64> {
  283. let mut total: u64 = 0;
  284. for entry in WalkDir::new(dir) {
  285. total += entry?.metadata()?.len();
  286. }
  287. Ok(total)
  288. }
  289. /// Writes a tar file to the given writer containing the given directory.
  290. fn create_tar_from_dir<P: AsRef<Path>, W: Write>(src_dir: P, dest_file: W) -> crate::Result<W> {
  291. let src_dir = src_dir.as_ref();
  292. let mut tar_builder = tar::Builder::new(dest_file);
  293. for entry in WalkDir::new(src_dir) {
  294. let entry = entry?;
  295. let src_path = entry.path();
  296. if src_path == src_dir {
  297. continue;
  298. }
  299. let dest_path = src_path.strip_prefix(src_dir)?;
  300. let stat = fs::metadata(src_path)?;
  301. let mut header = tar::Header::new_gnu();
  302. header.set_metadata_in_mode(&stat, HeaderMode::Deterministic);
  303. header.set_mtime(stat.mtime() as u64);
  304. if entry.file_type().is_dir() {
  305. tar_builder.append_data(&mut header, dest_path, &mut io::empty())?;
  306. } else {
  307. let mut src_file = fs::File::open(src_path)?;
  308. tar_builder.append_data(&mut header, dest_path, &mut src_file)?;
  309. }
  310. }
  311. let dest_file = tar_builder.into_inner()?;
  312. Ok(dest_file)
  313. }
  314. /// Creates a `.tar.gz` file from the given directory (placing the new file
  315. /// within the given directory's parent directory), then deletes the original
  316. /// directory and returns the path to the new file.
  317. fn tar_and_gzip_dir<P: AsRef<Path>>(src_dir: P) -> crate::Result<PathBuf> {
  318. let src_dir = src_dir.as_ref();
  319. let dest_path = src_dir.with_extension("tar.gz");
  320. let dest_file = common::create_file(&dest_path)?;
  321. let gzip_encoder = GzEncoder::new(dest_file, Compression::default());
  322. let gzip_encoder = create_tar_from_dir(src_dir, gzip_encoder)?;
  323. let mut dest_file = gzip_encoder.finish()?;
  324. dest_file.flush()?;
  325. Ok(dest_path)
  326. }
  327. /// Creates an `ar` archive from the given source files and writes it to the
  328. /// given destination path.
  329. fn create_archive(srcs: Vec<PathBuf>, dest: &Path) -> crate::Result<()> {
  330. let mut builder = ar::Builder::new(common::create_file(dest)?);
  331. for path in &srcs {
  332. builder.append_path(path)?;
  333. }
  334. builder.into_inner()?.flush()?;
  335. Ok(())
  336. }