deb_bundle.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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::{ResultExt, Settings};
  22. use ar;
  23. use icns;
  24. use image::png::{PNGDecoder, PNGEncoder};
  25. use image::{self, GenericImage, ImageDecoder};
  26. use libflate::gzip;
  27. use md5;
  28. use std::collections::BTreeSet;
  29. use std::ffi::OsStr;
  30. use std::fs::{self, File};
  31. use std::io::{self, Write};
  32. use std::path::{Path, PathBuf};
  33. use tar;
  34. use walkdir::WalkDir;
  35. pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
  36. let arch = match settings.binary_arch() {
  37. "x86" => "i386",
  38. "x86_64" => "amd64",
  39. other => other,
  40. };
  41. let package_base_name = format!(
  42. "{}_{}_{}",
  43. settings.binary_name(),
  44. settings.version_string(),
  45. arch
  46. );
  47. let package_name = format!("{}.deb", package_base_name);
  48. common::print_bundling(&package_name)?;
  49. let base_dir = settings.project_out_directory().join("bundle/deb");
  50. let package_dir = base_dir.join(&package_base_name);
  51. if package_dir.exists() {
  52. fs::remove_dir_all(&package_dir)
  53. .chain_err(|| format!("Failed to remove old {}", package_base_name))?;
  54. }
  55. let package_path = base_dir.join(package_name);
  56. // Generate data files.
  57. let data_dir = package_dir.join("data");
  58. let binary_dest = data_dir.join("usr/bin").join(settings.binary_name());
  59. common::copy_file(settings.binary_path(), &binary_dest)
  60. .chain_err(|| "Failed to copy binary file")?;
  61. transfer_resource_files(settings, &data_dir).chain_err(|| "Failed to copy resource files")?;
  62. generate_icon_files(settings, &data_dir).chain_err(|| "Failed to create icon files")?;
  63. generate_desktop_file(settings, &data_dir).chain_err(|| "Failed to create desktop file")?;
  64. // Generate control files.
  65. let control_dir = package_dir.join("control");
  66. generate_control_file(settings, arch, &control_dir, &data_dir)
  67. .chain_err(|| "Failed to create control file")?;
  68. generate_md5sums(&control_dir, &data_dir).chain_err(|| "Failed to create md5sums file")?;
  69. // Generate `debian-binary` file; see
  70. // http://www.tldp.org/HOWTO/Debian-Binary-Package-Building-HOWTO/x60.html#AEN66
  71. let debian_binary_path = package_dir.join("debian-binary");
  72. create_file_with_data(&debian_binary_path, "2.0\n")
  73. .chain_err(|| "Failed to create debian-binary file")?;
  74. // Apply tar/gzip/ar to create the final package file.
  75. let control_tar_gz_path =
  76. tar_and_gzip_dir(control_dir).chain_err(|| "Failed to tar/gzip control directory")?;
  77. let data_tar_gz_path =
  78. tar_and_gzip_dir(data_dir).chain_err(|| "Failed to tar/gzip data directory")?;
  79. create_archive(
  80. vec![debian_binary_path, control_tar_gz_path, data_tar_gz_path],
  81. &package_path,
  82. )
  83. .chain_err(|| "Failed to create package archive")?;
  84. Ok(vec![package_path])
  85. }
  86. /// Generate the application desktop file and store it under the `data_dir`.
  87. fn generate_desktop_file(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
  88. let bin_name = settings.binary_name();
  89. let desktop_file_name = format!("{}.desktop", bin_name);
  90. let desktop_file_path = data_dir
  91. .join("usr/share/applications")
  92. .join(desktop_file_name);
  93. let file = &mut common::create_file(&desktop_file_path)?;
  94. // For more information about the format of this file, see
  95. // https://developer.gnome.org/integration-guide/stable/desktop-files.html.en
  96. write!(file, "[Desktop Entry]\n")?;
  97. write!(file, "Encoding=UTF-8\n")?;
  98. if let Some(category) = settings.app_category() {
  99. write!(file, "Categories={}\n", category.gnome_desktop_categories())?;
  100. }
  101. if !settings.short_description().is_empty() {
  102. write!(file, "Comment={}\n", settings.short_description())?;
  103. }
  104. write!(file, "Exec={}\n", bin_name)?;
  105. write!(file, "Icon={}\n", bin_name)?;
  106. write!(file, "Name={}\n", settings.bundle_name())?;
  107. write!(file, "Terminal=false\n")?;
  108. write!(file, "Type=Application\n")?;
  109. write!(file, "Version={}\n", settings.version_string())?;
  110. Ok(())
  111. }
  112. fn generate_control_file(
  113. settings: &Settings,
  114. arch: &str,
  115. control_dir: &Path,
  116. data_dir: &Path,
  117. ) -> crate::Result<()> {
  118. // For more information about the format of this file, see
  119. // https://www.debian.org/doc/debian-policy/ch-controlfields.html
  120. let dest_path = control_dir.join("control");
  121. let mut file = common::create_file(&dest_path)?;
  122. writeln!(
  123. &mut file,
  124. "Package: {}",
  125. str::replace(settings.bundle_name(), " ", "-").to_ascii_lowercase()
  126. )?;
  127. writeln!(&mut file, "Version: {}", settings.version_string())?;
  128. writeln!(&mut file, "Architecture: {}", arch)?;
  129. writeln!(&mut file, "Installed-Size: {}", total_dir_size(data_dir)?)?;
  130. let authors = settings.authors_comma_separated().unwrap_or(String::new());
  131. writeln!(&mut file, "Maintainer: {}", authors)?;
  132. if !settings.homepage_url().is_empty() {
  133. writeln!(&mut file, "Homepage: {}", settings.homepage_url())?;
  134. }
  135. let dependencies = settings.debian_dependencies();
  136. if !dependencies.is_empty() {
  137. writeln!(&mut file, "Depends: {}", dependencies.join(", "))?;
  138. }
  139. let mut short_description = settings.short_description().trim();
  140. if short_description.is_empty() {
  141. short_description = "(none)";
  142. }
  143. let mut long_description = settings.long_description().unwrap_or("").trim();
  144. if long_description.is_empty() {
  145. long_description = "(none)";
  146. }
  147. writeln!(&mut file, "Description: {}", short_description)?;
  148. for line in long_description.lines() {
  149. let line = line.trim();
  150. if line.is_empty() {
  151. writeln!(&mut file, " .")?;
  152. } else {
  153. writeln!(&mut file, " {}", line)?;
  154. }
  155. }
  156. file.flush()?;
  157. Ok(())
  158. }
  159. /// Create an `md5sums` file in the `control_dir` containing the MD5 checksums
  160. /// for each file within the `data_dir`.
  161. fn generate_md5sums(control_dir: &Path, data_dir: &Path) -> crate::Result<()> {
  162. let md5sums_path = control_dir.join("md5sums");
  163. let mut md5sums_file = common::create_file(&md5sums_path)?;
  164. for entry in WalkDir::new(data_dir) {
  165. let entry = entry?;
  166. let path = entry.path();
  167. if path.is_dir() {
  168. continue;
  169. }
  170. let mut file = File::open(path)?;
  171. let mut hash = md5::Context::new();
  172. io::copy(&mut file, &mut hash)?;
  173. for byte in hash.compute().iter() {
  174. write!(md5sums_file, "{:02x}", byte)?;
  175. }
  176. let rel_path = path.strip_prefix(data_dir).unwrap();
  177. let path_str = rel_path.to_str().ok_or_else(|| {
  178. let msg = format!("Non-UTF-8 path: {:?}", rel_path);
  179. io::Error::new(io::ErrorKind::InvalidData, msg)
  180. })?;
  181. write!(md5sums_file, " {}\n", path_str)?;
  182. }
  183. Ok(())
  184. }
  185. /// Copy the bundle's resource files into an appropriate directory under the
  186. /// `data_dir`.
  187. fn transfer_resource_files(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
  188. let resource_dir = data_dir.join("usr/lib").join(settings.binary_name());
  189. for src in settings.resource_files() {
  190. let src = src?;
  191. let dest = resource_dir.join(common::resource_relpath(&src));
  192. common::copy_file(&src, &dest)
  193. .chain_err(|| format!("Failed to copy resource file {:?}", src))?;
  194. }
  195. Ok(())
  196. }
  197. /// Generate the icon files and store them under the `data_dir`.
  198. fn generate_icon_files(settings: &Settings, data_dir: &PathBuf) -> crate::Result<()> {
  199. let base_dir = data_dir.join("usr/share/icons/hicolor");
  200. let get_dest_path = |width: u32, height: u32, is_high_density: bool| {
  201. base_dir.join(format!(
  202. "{}x{}{}/apps/{}.png",
  203. width,
  204. height,
  205. if is_high_density { "@2x" } else { "" },
  206. settings.binary_name()
  207. ))
  208. };
  209. let mut sizes = BTreeSet::new();
  210. // Prefer PNG files.
  211. for icon_path in settings.icon_files() {
  212. let icon_path = icon_path?;
  213. if icon_path.extension() != Some(OsStr::new("png")) {
  214. continue;
  215. }
  216. let mut decoder = PNGDecoder::new(File::open(&icon_path)?);
  217. let (width, height) = decoder.dimensions()?;
  218. let is_high_density = common::is_retina(&icon_path);
  219. if !sizes.contains(&(width, height, is_high_density)) {
  220. sizes.insert((width, height, is_high_density));
  221. let dest_path = get_dest_path(width, height, is_high_density);
  222. common::copy_file(&icon_path, &dest_path)?;
  223. }
  224. }
  225. // Fall back to non-PNG files for any missing sizes.
  226. for icon_path in settings.icon_files() {
  227. let icon_path = icon_path?;
  228. if icon_path.extension() == Some(OsStr::new("png")) {
  229. continue;
  230. } else if icon_path.extension() == Some(OsStr::new("icns")) {
  231. let icon_family = icns::IconFamily::read(File::open(&icon_path)?)?;
  232. for icon_type in icon_family.available_icons() {
  233. let width = icon_type.screen_width();
  234. let height = icon_type.screen_height();
  235. let is_high_density = icon_type.pixel_density() > 1;
  236. if !sizes.contains(&(width, height, is_high_density)) {
  237. sizes.insert((width, height, is_high_density));
  238. let dest_path = get_dest_path(width, height, is_high_density);
  239. let icon = icon_family.get_icon_with_type(icon_type)?;
  240. icon.write_png(common::create_file(&dest_path)?)?;
  241. }
  242. }
  243. } else {
  244. let icon = r#try!(image::open(&icon_path));
  245. let (width, height) = icon.dimensions();
  246. let is_high_density = common::is_retina(&icon_path);
  247. if !sizes.contains(&(width, height, is_high_density)) {
  248. sizes.insert((width, height, is_high_density));
  249. let dest_path = get_dest_path(width, height, is_high_density);
  250. let encoder = PNGEncoder::new(common::create_file(&dest_path)?);
  251. encoder.encode(&icon.raw_pixels(), width, height, icon.color())?;
  252. }
  253. }
  254. }
  255. Ok(())
  256. }
  257. /// Create an empty file at the given path, creating any parent directories as
  258. /// needed, then write `data` into the file.
  259. fn create_file_with_data<P: AsRef<Path>>(path: P, data: &str) -> crate::Result<()> {
  260. let mut file = common::create_file(path.as_ref())?;
  261. file.write_all(data.as_bytes())?;
  262. file.flush()?;
  263. Ok(())
  264. }
  265. /// Computes the total size, in bytes, of the given directory and all of its
  266. /// contents.
  267. fn total_dir_size(dir: &Path) -> crate::Result<u64> {
  268. let mut total: u64 = 0;
  269. for entry in WalkDir::new(&dir) {
  270. total += entry?.metadata()?.len();
  271. }
  272. Ok(total)
  273. }
  274. /// Writes a tar file to the given writer containing the given directory.
  275. fn create_tar_from_dir<P: AsRef<Path>, W: Write>(src_dir: P, dest_file: W) -> crate::Result<W> {
  276. let src_dir = src_dir.as_ref();
  277. let mut tar_builder = tar::Builder::new(dest_file);
  278. for entry in WalkDir::new(&src_dir) {
  279. let entry = entry?;
  280. let src_path = entry.path();
  281. if src_path == src_dir {
  282. continue;
  283. }
  284. let dest_path = src_path.strip_prefix(&src_dir).unwrap();
  285. if entry.file_type().is_dir() {
  286. tar_builder.append_dir(dest_path, src_path)?;
  287. } else {
  288. let mut src_file = fs::File::open(src_path)?;
  289. tar_builder.append_file(dest_path, &mut src_file)?;
  290. }
  291. }
  292. let dest_file = tar_builder.into_inner()?;
  293. Ok(dest_file)
  294. }
  295. /// Creates a `.tar.gz` file from the given directory (placing the new file
  296. /// within the given directory's parent directory), then deletes the original
  297. /// directory and returns the path to the new file.
  298. fn tar_and_gzip_dir<P: AsRef<Path>>(src_dir: P) -> crate::Result<PathBuf> {
  299. let src_dir = src_dir.as_ref();
  300. let dest_path = src_dir.with_extension("tar.gz");
  301. let dest_file = common::create_file(&dest_path)?;
  302. let gzip_encoder = gzip::Encoder::new(dest_file)?;
  303. let gzip_encoder = create_tar_from_dir(src_dir, gzip_encoder)?;
  304. let mut dest_file = gzip_encoder.finish().into_result()?;
  305. dest_file.flush()?;
  306. Ok(dest_path)
  307. }
  308. /// Creates an `ar` archive from the given source files and writes it to the
  309. /// given destination path.
  310. fn create_archive(srcs: Vec<PathBuf>, dest: &Path) -> crate::Result<()> {
  311. let mut builder = ar::Builder::new(common::create_file(&dest)?);
  312. for path in &srcs {
  313. builder.append_path(path)?;
  314. }
  315. builder.into_inner()?.flush()?;
  316. Ok(())
  317. }