deb_bundle.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. // The structure of a Debian package looks something like this:
  2. //
  3. // foobar_1.2.3_i386.deb # Actually an ar archive
  4. // debian-binary # Specifies deb format version (2.0 in our case)
  5. // control.tar.gz # Contains files controlling the installation:
  6. // control # Basic package metadata
  7. // md5sums # Checksums for files in data.tar.gz below
  8. // postinst # Post-installation script (optional)
  9. // prerm # Pre-uninstallation script (optional)
  10. // data.tar.gz # Contains files to be installed:
  11. // usr/bin/foobar # Binary executable file
  12. // usr/share/applications/foobar.desktop # Desktop file (for apps)
  13. // usr/share/icons/hicolor/... # Icon files (for apps)
  14. // usr/lib/foobar/... # Other resource files
  15. //
  16. // For cargo-bundle, we put bundle resource files under /usr/lib/package_name/,
  17. // and then generate the desktop file and control file from the bundle
  18. // metadata, as well as generating the md5sums file. Currently we do not
  19. // generate postinst or prerm files.
  20. use super::common;
  21. use crate::Settings;
  22. use anyhow::Context;
  23. use ar;
  24. use icns;
  25. use image::png::PngDecoder;
  26. use image::{self, GenericImageView, ImageDecoder};
  27. use libflate::gzip;
  28. use md5;
  29. use std::process::{Command, Stdio};
  30. use tar;
  31. use walkdir::WalkDir;
  32. use std::collections::BTreeSet;
  33. use std::ffi::OsStr;
  34. use std::fs::{self, File};
  35. use std::io::{self, Write};
  36. use std::path::{Path, PathBuf};
  37. /// Bundles the project.
  38. /// Returns a vector of PathBuf that shows where the DEB was created.
  39. pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
  40. let arch = match settings.binary_arch() {
  41. "x86" => "i386",
  42. "x86_64" => "amd64",
  43. other => other,
  44. };
  45. let package_base_name = format!(
  46. "{}_{}_{}",
  47. settings.binary_name(),
  48. settings.version_string(),
  49. arch
  50. );
  51. let package_name = format!("{}.deb", package_base_name);
  52. common::print_bundling(&package_name)?;
  53. let base_dir = settings.project_out_directory().join("bundle/deb");
  54. let package_dir = base_dir.join(&package_base_name);
  55. if package_dir.exists() {
  56. fs::remove_dir_all(&package_dir)
  57. .with_context(|| format!("Failed to remove old {}", package_base_name))?;
  58. }
  59. let package_path = base_dir.join(package_name);
  60. let data_dir = generate_data(settings, &package_dir)
  61. .with_context(|| "Failed to build data folders and files")?;
  62. // Generate control files.
  63. let control_dir = package_dir.join("control");
  64. generate_control_file(settings, arch, &control_dir, &data_dir)
  65. .with_context(|| "Failed to create control file")?;
  66. generate_md5sums(&control_dir, &data_dir).with_context(|| "Failed to create md5sums file")?;
  67. // Generate `debian-binary` file; see
  68. // http://www.tldp.org/HOWTO/Debian-Binary-Package-Building-HOWTO/x60.html#AEN66
  69. let debian_binary_path = package_dir.join("debian-binary");
  70. create_file_with_data(&debian_binary_path, "2.0\n")
  71. .with_context(|| "Failed to create debian-binary file")?;
  72. // Apply tar/gzip/ar to create the final package file.
  73. let control_tar_gz_path =
  74. tar_and_gzip_dir(control_dir).with_context(|| "Failed to tar/gzip control directory")?;
  75. let data_tar_gz_path =
  76. tar_and_gzip_dir(data_dir).with_context(|| "Failed to tar/gzip data directory")?;
  77. create_archive(
  78. vec![debian_binary_path, control_tar_gz_path, data_tar_gz_path],
  79. &package_path,
  80. )
  81. .with_context(|| "Failed to create package archive")?;
  82. Ok(vec![package_path])
  83. }
  84. /// Generate the debian data folders and files.
  85. pub fn generate_data(settings: &Settings, package_dir: &Path) -> crate::Result<PathBuf> {
  86. // Generate data files.
  87. let data_dir = package_dir.join("data");
  88. let bin_name = settings.binary_name();
  89. let binary_dest = data_dir.join("usr/bin").join(bin_name);
  90. let bin_dir = data_dir.join("usr/bin");
  91. common::copy_file(settings.binary_path(), &binary_dest)
  92. .with_context(|| "Failed to copy binary file")?;
  93. transfer_resource_files(settings, &data_dir).with_context(|| "Failed to copy resource files")?;
  94. settings
  95. .copy_binaries(&bin_dir)
  96. .with_context(|| "Failed to copy external binaries")?;
  97. generate_icon_files(settings, &data_dir).with_context(|| "Failed to create icon files")?;
  98. generate_desktop_file(settings, &data_dir).with_context(|| "Failed to create desktop file")?;
  99. let use_bootstrapper = settings.debian_use_bootstrapper();
  100. if use_bootstrapper {
  101. generate_bootstrap_file(settings, &data_dir)
  102. .with_context(|| "Failed to generate bootstrap file")?;
  103. }
  104. Ok(data_dir)
  105. }
  106. /// Generates the bootstrap script file.
  107. fn generate_bootstrap_file(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
  108. let bin_name = settings.binary_name();
  109. let bin_dir = data_dir.join("usr/bin");
  110. let bootstrap_file_name = format!("__{}-bootstrapper", bin_name);
  111. let bootstrapper_file_path = bin_dir.join(bootstrap_file_name.clone());
  112. let bootstrapper_file = &mut common::create_file(&bootstrapper_file_path)?;
  113. write!(
  114. bootstrapper_file,
  115. "#!/usr/bin/env sh
  116. # This bootstraps the environment for Tauri, so environments are available.
  117. export NVM_DIR=\"$([ -z \"${{XDG_CONFIG_HOME-}}\" ] && printf %s \"${{HOME}}/.nvm\" || printf %s \"${{XDG_CONFIG_HOME}}/nvm\")\"
  118. [ -s \"$NVM_DIR/nvm.sh\" ] && . \"$NVM_DIR/nvm.sh\"
  119. if [ -e ~/.bash_profile ]
  120. then
  121. source ~/.bash_profile
  122. fi
  123. if [ -e ~/.zprofile ]
  124. then
  125. source ~/.zprofile
  126. fi
  127. if [ -e ~/.profile ]
  128. then
  129. source ~/.profile
  130. fi
  131. if [ -e ~/.bashrc ]
  132. then
  133. source ~/.bashrc
  134. fi
  135. if [ -e ~/.zshrc ]
  136. then
  137. source ~/.zshrc
  138. fi
  139. echo $PATH
  140. source /etc/profile
  141. if pidof -x \"{}\" >/dev/null; then
  142. exit 0
  143. else
  144. Exec=/usr/bin/env /usr/bin/{} $@ & disown
  145. fi
  146. exit 0",
  147. bootstrap_file_name, bin_name
  148. )?;
  149. bootstrapper_file.flush()?;
  150. Command::new("chmod")
  151. .arg("+x")
  152. .arg(bootstrap_file_name)
  153. .current_dir(&bin_dir)
  154. .stdout(Stdio::piped())
  155. .stderr(Stdio::piped())
  156. .spawn()?;
  157. Ok(())
  158. }
  159. /// Generate the application desktop file and store it under the `data_dir`.
  160. fn generate_desktop_file(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
  161. let bin_name = settings.binary_name();
  162. let desktop_file_name = format!("{}.desktop", bin_name);
  163. let desktop_file_path = data_dir
  164. .join("usr/share/applications")
  165. .join(desktop_file_name);
  166. let file = &mut common::create_file(&desktop_file_path)?;
  167. // For more information about the format of this file, see
  168. // https://developer.gnome.org/integration-guide/stable/desktop-files.html.en
  169. write!(file, "[Desktop Entry]\n")?;
  170. write!(file, "Encoding=UTF-8\n")?;
  171. if let Some(category) = settings.app_category() {
  172. write!(file, "Categories={}\n", category.gnome_desktop_categories())?;
  173. }
  174. if !settings.short_description().is_empty() {
  175. write!(file, "Comment={}\n", settings.short_description())?;
  176. }
  177. let use_bootstrapper = settings.debian_use_bootstrapper();
  178. write!(
  179. file,
  180. "Exec={}\n",
  181. if use_bootstrapper {
  182. format!("__{}-bootstrapper", bin_name)
  183. } else {
  184. bin_name.to_string()
  185. }
  186. )?;
  187. write!(file, "Icon={}\n", bin_name)?;
  188. write!(file, "Name={}\n", settings.bundle_name())?;
  189. write!(file, "Terminal=false\n")?;
  190. write!(file, "Type=Application\n")?;
  191. write!(file, "Version={}\n", settings.version_string())?;
  192. Ok(())
  193. }
  194. /// Generates the debian control file and stores it under the `control_dir`.
  195. fn generate_control_file(
  196. settings: &Settings,
  197. arch: &str,
  198. control_dir: &Path,
  199. data_dir: &Path,
  200. ) -> crate::Result<()> {
  201. // For more information about the format of this file, see
  202. // https://www.debian.org/doc/debian-policy/ch-controlfields.html
  203. let dest_path = control_dir.join("control");
  204. let mut file = common::create_file(&dest_path)?;
  205. writeln!(
  206. &mut file,
  207. "Package: {}",
  208. str::replace(settings.bundle_name(), " ", "-").to_ascii_lowercase()
  209. )?;
  210. writeln!(&mut file, "Version: {}", settings.version_string())?;
  211. writeln!(&mut file, "Architecture: {}", arch)?;
  212. writeln!(&mut file, "Installed-Size: {}", total_dir_size(data_dir)?)?;
  213. let authors = settings.authors_comma_separated().unwrap_or(String::new());
  214. writeln!(&mut file, "Maintainer: {}", authors)?;
  215. if !settings.homepage_url().is_empty() {
  216. writeln!(&mut file, "Homepage: {}", settings.homepage_url())?;
  217. }
  218. let dependencies = settings.debian_dependencies();
  219. if !dependencies.is_empty() {
  220. writeln!(&mut file, "Depends: {}", dependencies.join(", "))?;
  221. }
  222. let mut short_description = settings.short_description().trim();
  223. if short_description.is_empty() {
  224. short_description = "(none)";
  225. }
  226. let mut long_description = settings.long_description().unwrap_or("").trim();
  227. if long_description.is_empty() {
  228. long_description = "(none)";
  229. }
  230. writeln!(&mut file, "Description: {}", short_description)?;
  231. for line in long_description.lines() {
  232. let line = line.trim();
  233. if line.is_empty() {
  234. writeln!(&mut file, " .")?;
  235. } else {
  236. writeln!(&mut file, " {}", line)?;
  237. }
  238. }
  239. file.flush()?;
  240. Ok(())
  241. }
  242. /// Create an `md5sums` file in the `control_dir` containing the MD5 checksums
  243. /// for each file within the `data_dir`.
  244. fn generate_md5sums(control_dir: &Path, data_dir: &Path) -> crate::Result<()> {
  245. let md5sums_path = control_dir.join("md5sums");
  246. let mut md5sums_file = common::create_file(&md5sums_path)?;
  247. for entry in WalkDir::new(data_dir) {
  248. let entry = entry?;
  249. let path = entry.path();
  250. if path.is_dir() {
  251. continue;
  252. }
  253. let mut file = File::open(path)?;
  254. let mut hash = md5::Context::new();
  255. io::copy(&mut file, &mut hash)?;
  256. for byte in hash.compute().iter() {
  257. write!(md5sums_file, "{:02x}", byte)?;
  258. }
  259. let rel_path = path.strip_prefix(data_dir)?;
  260. let path_str = rel_path.to_str().ok_or_else(|| {
  261. let msg = format!("Non-UTF-8 path: {:?}", rel_path);
  262. io::Error::new(io::ErrorKind::InvalidData, msg)
  263. })?;
  264. write!(md5sums_file, " {}\n", path_str)?;
  265. }
  266. Ok(())
  267. }
  268. /// Copy the bundle's resource files into an appropriate directory under the
  269. /// `data_dir`.
  270. fn transfer_resource_files(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
  271. let resource_dir = data_dir.join("usr/lib").join(settings.binary_name());
  272. settings.copy_resources(&resource_dir)
  273. }
  274. /// Generate the icon files and store them under the `data_dir`.
  275. fn generate_icon_files(settings: &Settings, data_dir: &PathBuf) -> crate::Result<()> {
  276. let base_dir = data_dir.join("usr/share/icons/hicolor");
  277. let get_dest_path = |width: u32, height: u32, is_high_density: bool| {
  278. base_dir.join(format!(
  279. "{}x{}{}/apps/{}.png",
  280. width,
  281. height,
  282. if is_high_density { "@2x" } else { "" },
  283. settings.binary_name()
  284. ))
  285. };
  286. let mut sizes = BTreeSet::new();
  287. // Prefer PNG files.
  288. for icon_path in settings.icon_files() {
  289. let icon_path = icon_path?;
  290. if icon_path.extension() != Some(OsStr::new("png")) {
  291. continue;
  292. }
  293. let decoder = PngDecoder::new(File::open(&icon_path)?)?;
  294. let width = decoder.dimensions().0;
  295. let height = decoder.dimensions().1;
  296. let is_high_density = common::is_retina(&icon_path);
  297. if !sizes.contains(&(width, height, is_high_density)) {
  298. sizes.insert((width, height, is_high_density));
  299. let dest_path = get_dest_path(width, height, is_high_density);
  300. common::copy_file(&icon_path, &dest_path)?;
  301. }
  302. }
  303. // Fall back to non-PNG files for any missing sizes.
  304. for icon_path in settings.icon_files() {
  305. let icon_path = icon_path?;
  306. if icon_path.extension() == Some(OsStr::new("png")) {
  307. continue;
  308. } else if icon_path.extension() == Some(OsStr::new("icns")) {
  309. let icon_family = icns::IconFamily::read(File::open(&icon_path)?)?;
  310. for icon_type in icon_family.available_icons() {
  311. let width = icon_type.screen_width();
  312. let height = icon_type.screen_height();
  313. let is_high_density = icon_type.pixel_density() > 1;
  314. if !sizes.contains(&(width, height, is_high_density)) {
  315. sizes.insert((width, height, is_high_density));
  316. let dest_path = get_dest_path(width, height, is_high_density);
  317. let icon = icon_family.get_icon_with_type(icon_type)?;
  318. icon.write_png(common::create_file(&dest_path)?)?;
  319. }
  320. }
  321. } else {
  322. let icon = image::open(&icon_path)?;
  323. let (width, height) = icon.dimensions();
  324. let is_high_density = common::is_retina(&icon_path);
  325. if !sizes.contains(&(width, height, is_high_density)) {
  326. sizes.insert((width, height, is_high_density));
  327. let dest_path = get_dest_path(width, height, is_high_density);
  328. icon.write_to(
  329. &mut common::create_file(&dest_path)?,
  330. image::ImageOutputFormat::Png,
  331. )?;
  332. }
  333. }
  334. }
  335. Ok(())
  336. }
  337. /// Create an empty file at the given path, creating any parent directories as
  338. /// needed, then write `data` into the file.
  339. fn create_file_with_data<P: AsRef<Path>>(path: P, data: &str) -> crate::Result<()> {
  340. let mut file = common::create_file(path.as_ref())?;
  341. file.write_all(data.as_bytes())?;
  342. file.flush()?;
  343. Ok(())
  344. }
  345. /// Computes the total size, in bytes, of the given directory and all of its
  346. /// contents.
  347. fn total_dir_size(dir: &Path) -> crate::Result<u64> {
  348. let mut total: u64 = 0;
  349. for entry in WalkDir::new(&dir) {
  350. total += entry?.metadata()?.len();
  351. }
  352. Ok(total)
  353. }
  354. /// Writes a tar file to the given writer containing the given directory.
  355. fn create_tar_from_dir<P: AsRef<Path>, W: Write>(src_dir: P, dest_file: W) -> crate::Result<W> {
  356. let src_dir = src_dir.as_ref();
  357. let mut tar_builder = tar::Builder::new(dest_file);
  358. for entry in WalkDir::new(&src_dir) {
  359. let entry = entry?;
  360. let src_path = entry.path();
  361. if src_path == src_dir {
  362. continue;
  363. }
  364. let dest_path = src_path.strip_prefix(&src_dir)?;
  365. if entry.file_type().is_dir() {
  366. tar_builder.append_dir(dest_path, src_path)?;
  367. } else {
  368. let mut src_file = fs::File::open(src_path)?;
  369. tar_builder.append_file(dest_path, &mut src_file)?;
  370. }
  371. }
  372. let dest_file = tar_builder.into_inner()?;
  373. Ok(dest_file)
  374. }
  375. /// Creates a `.tar.gz` file from the given directory (placing the new file
  376. /// within the given directory's parent directory), then deletes the original
  377. /// directory and returns the path to the new file.
  378. fn tar_and_gzip_dir<P: AsRef<Path>>(src_dir: P) -> crate::Result<PathBuf> {
  379. let src_dir = src_dir.as_ref();
  380. let dest_path = src_dir.with_extension("tar.gz");
  381. let dest_file = common::create_file(&dest_path)?;
  382. let gzip_encoder = gzip::Encoder::new(dest_file)?;
  383. let gzip_encoder = create_tar_from_dir(src_dir, gzip_encoder)?;
  384. let mut dest_file = gzip_encoder.finish().into_result()?;
  385. dest_file.flush()?;
  386. Ok(dest_path)
  387. }
  388. /// Creates an `ar` archive from the given source files and writes it to the
  389. /// given destination path.
  390. fn create_archive(srcs: Vec<PathBuf>, dest: &Path) -> crate::Result<()> {
  391. let mut builder = ar::Builder::new(common::create_file(&dest)?);
  392. for path in &srcs {
  393. builder.append_path(path)?;
  394. }
  395. builder.into_inner()?.flush()?;
  396. Ok(())
  397. }