From 7e6620b71d739e0d7d8bcd487fc5e66d69450059 Mon Sep 17 00:00:00 2001 From: amr-crabnebula Date: Fri, 8 Sep 2023 03:44:06 +0300 Subject: [PATCH] feat: implement `deb` --- Cargo.lock | 294 +++++++++++++++++++- crates/config/Cargo.toml | 1 + crates/config/src/category.rs | 465 ++++++++++++++++++++++++++++++++ crates/config/src/lib.rs | 60 +---- crates/packager/Cargo.toml | 9 + crates/packager/schema.json | 92 +++---- crates/packager/src/config.rs | 56 +++- crates/packager/src/deb/mod.rs | 358 +++++++++++++++++++++++- crates/packager/src/error.rs | 28 +- crates/packager/src/main.rs | 131 ++++----- crates/packager/src/nsis/mod.rs | 6 +- crates/packager/src/util.rs | 36 +++ crates/packager/src/wix/mod.rs | 16 +- examples/dioxus/Cargo.toml | 4 + examples/tauri/Cargo.toml | 5 + 15 files changed, 1357 insertions(+), 204 deletions(-) create mode 100644 crates/config/src/category.rs diff --git a/Cargo.lock b/Cargo.lock index d0f42c52..5851c8ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "aho-corasick" version = "1.0.5" @@ -110,6 +116,12 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "ar" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d67af77d68a931ecd5cbd8a3b5987d63a1d1d1278f7f6a60ae33db485cdebb69" + [[package]] name = "async-channel" version = "1.9.0" @@ -241,6 +253,12 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -369,6 +387,7 @@ dependencies = [ name = "cargo-packager" version = "0.0.0" dependencies = [ + "ar", "cargo-packager-config", "cargo_metadata", "clap", @@ -377,8 +396,12 @@ dependencies = [ "env_logger", "glob", "handlebars", + "heck", "hex", + "image", + "libflate", "log", + "md5", "once_cell", "regex", "relative-path", @@ -388,9 +411,11 @@ dependencies = [ "serde_json", "sha1", "sha2", + "tar", "thiserror", "ureq", "uuid", + "walkdir", "windows-sys 0.48.0", "winreg 0.51.0", "zip", @@ -403,6 +428,7 @@ dependencies = [ "clap", "schemars", "serde", + "strsim", ] [[package]] @@ -673,6 +699,30 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -682,6 +732,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1024,6 +1080,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "embed-resource" version = "2.3.0" @@ -1138,6 +1200,22 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "exr" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e481eb11a482815d3e9d618db8c42a93207134662873809335a92327440c18" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1172,6 +1250,18 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "windows-sys 0.48.0", +] + [[package]] name = "flate2" version = "1.0.27" @@ -1182,6 +1272,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1445,8 +1548,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", ] [[package]] @@ -1626,6 +1741,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + [[package]] name = "handlebars" version = "4.4.0" @@ -1844,8 +1968,14 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", + "exr", + "gif", + "jpeg-decoder", "num-rational", "num-traits", + "png", + "qoi", + "tiff", ] [[package]] @@ -2011,6 +2141,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.64" @@ -2074,12 +2213,38 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "libflate" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77", +] + +[[package]] +name = "libflate_lz77" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf" +dependencies = [ + "rle-decode-fast", +] + [[package]] name = "line-wrap" version = "0.1.1" @@ -2199,6 +2364,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.6.3" @@ -2241,6 +2412,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.10", +] + [[package]] name = "ndk" version = "0.6.0" @@ -2641,6 +2821,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -2763,6 +2963,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-xml" version = "0.29.0" @@ -2868,6 +3077,28 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -3017,12 +3248,18 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", + "spin 0.5.2", "untrusted", "web-sys", "winapi", ] +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -3485,6 +3722,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spinning" version = "0.1.0" @@ -3713,6 +3959,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.11" @@ -3988,6 +4245,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.28" @@ -4605,6 +4873,12 @@ dependencies = [ "windows-metadata", ] +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "winapi" version = "0.3.9" @@ -4945,6 +5219,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" +dependencies = [ + "libc", +] + [[package]] name = "zip" version = "0.6.6" @@ -4956,3 +5239,12 @@ dependencies = [ "crossbeam-utils", "flate2", ] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 671ab232..5aecc752 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -12,3 +12,4 @@ clap = ["dep:clap"] serde.workspace = true schemars.workspace = true clap = { workspace = true, optional = true } +strsim = "0.10" diff --git a/crates/config/src/category.rs b/crates/config/src/category.rs new file mode 100644 index 00000000..e9b0a508 --- /dev/null +++ b/crates/config/src/category.rs @@ -0,0 +1,465 @@ +use std::{fmt, str::FromStr}; + +use schemars::JsonSchema; +use serde::Serialize; + +const CONFIDENCE_THRESHOLD: f64 = 0.8; + +const MACOS_APP_CATEGORY_PREFIX: &str = "public.app-category."; + +/// The possible app categories. +/// Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, JsonSchema)] +#[non_exhaustive] +pub enum AppCategory { + Business, + DeveloperTool, + Education, + Entertainment, + Finance, + Game, + ActionGame, + AdventureGame, + ArcadeGame, + BoardGame, + CardGame, + CasinoGame, + DiceGame, + EducationalGame, + FamilyGame, + KidsGame, + MusicGame, + PuzzleGame, + RacingGame, + RolePlayingGame, + SimulationGame, + SportsGame, + StrategyGame, + TriviaGame, + WordGame, + GraphicsAndDesign, + HealthcareAndFitness, + Lifestyle, + Medical, + Music, + News, + Photography, + Productivity, + Reference, + SocialNetworking, + Sports, + Travel, + Utility, + Video, + Weather, +} + +impl FromStr for AppCategory { + type Err = Option<&'static str>; + + /// Given a string, returns the `AppCategory` it refers to, or the closest + /// string that the user might have intended (if any). + fn from_str(input: &str) -> Result { + // Canonicalize input: + let mut input = input.to_ascii_lowercase(); + if input.starts_with(MACOS_APP_CATEGORY_PREFIX) { + input = input + .split_at(MACOS_APP_CATEGORY_PREFIX.len()) + .1 + .to_string(); + } + input = input.replace(' ', ""); + input = input.replace('-', ""); + + // Find best match: + let mut best_confidence = 0.0; + let mut best_category: Option = None; + for &(string, category) in CATEGORY_STRINGS.iter() { + if input == string { + return Ok(category); + } + let confidence = strsim::jaro_winkler(&input, string); + if confidence >= CONFIDENCE_THRESHOLD && confidence > best_confidence { + best_confidence = confidence; + best_category = Some(category); + } + } + Err(best_category.map(AppCategory::canonical)) + } +} + +impl AppCategory { + /// Map an AppCategory to the string we recommend to use in Cargo.toml if + /// the users misspells the category name. + fn canonical(self) -> &'static str { + match self { + AppCategory::Business => "Business", + AppCategory::DeveloperTool => "Developer Tool", + AppCategory::Education => "Education", + AppCategory::Entertainment => "Entertainment", + AppCategory::Finance => "Finance", + AppCategory::Game => "Game", + AppCategory::ActionGame => "Action Game", + AppCategory::AdventureGame => "Adventure Game", + AppCategory::ArcadeGame => "Arcade Game", + AppCategory::BoardGame => "Board Game", + AppCategory::CardGame => "Card Game", + AppCategory::CasinoGame => "Casino Game", + AppCategory::DiceGame => "Dice Game", + AppCategory::EducationalGame => "Educational Game", + AppCategory::FamilyGame => "Family Game", + AppCategory::KidsGame => "Kids Game", + AppCategory::MusicGame => "Music Game", + AppCategory::PuzzleGame => "Puzzle Game", + AppCategory::RacingGame => "Racing Game", + AppCategory::RolePlayingGame => "Role-Playing Game", + AppCategory::SimulationGame => "Simulation Game", + AppCategory::SportsGame => "Sports Game", + AppCategory::StrategyGame => "Strategy Game", + AppCategory::TriviaGame => "Trivia Game", + AppCategory::WordGame => "Word Game", + AppCategory::GraphicsAndDesign => "Graphics and Design", + AppCategory::HealthcareAndFitness => "Healthcare and Fitness", + AppCategory::Lifestyle => "Lifestyle", + AppCategory::Medical => "Medical", + AppCategory::Music => "Music", + AppCategory::News => "News", + AppCategory::Photography => "Photography", + AppCategory::Productivity => "Productivity", + AppCategory::Reference => "Reference", + AppCategory::SocialNetworking => "Social Networking", + AppCategory::Sports => "Sports", + AppCategory::Travel => "Travel", + AppCategory::Utility => "Utility", + AppCategory::Video => "Video", + AppCategory::Weather => "Weather", + } + } + + /// Map an AppCategory to the closest set of GNOME desktop registered + /// categories that matches that category. + pub fn gnome_desktop_categories(self) -> &'static str { + match &self { + AppCategory::Business => "Office;", + AppCategory::DeveloperTool => "Development;", + AppCategory::Education => "Education;", + AppCategory::Entertainment => "Network;", + AppCategory::Finance => "Office;Finance;", + AppCategory::Game => "Game;", + AppCategory::ActionGame => "Game;ActionGame;", + AppCategory::AdventureGame => "Game;AdventureGame;", + AppCategory::ArcadeGame => "Game;ArcadeGame;", + AppCategory::BoardGame => "Game;BoardGame;", + AppCategory::CardGame => "Game;CardGame;", + AppCategory::CasinoGame => "Game;", + AppCategory::DiceGame => "Game;", + AppCategory::EducationalGame => "Game;Education;", + AppCategory::FamilyGame => "Game;", + AppCategory::KidsGame => "Game;KidsGame;", + AppCategory::MusicGame => "Game;", + AppCategory::PuzzleGame => "Game;LogicGame;", + AppCategory::RacingGame => "Game;", + AppCategory::RolePlayingGame => "Game;RolePlaying;", + AppCategory::SimulationGame => "Game;Simulation;", + AppCategory::SportsGame => "Game;SportsGame;", + AppCategory::StrategyGame => "Game;StrategyGame;", + AppCategory::TriviaGame => "Game;", + AppCategory::WordGame => "Game;", + AppCategory::GraphicsAndDesign => "Graphics;", + AppCategory::HealthcareAndFitness => "Science;", + AppCategory::Lifestyle => "Education;", + AppCategory::Medical => "Science;MedicalSoftware;", + AppCategory::Music => "AudioVideo;Audio;Music;", + AppCategory::News => "Network;News;", + AppCategory::Photography => "Graphics;Photography;", + AppCategory::Productivity => "Office;", + AppCategory::Reference => "Education;", + AppCategory::SocialNetworking => "Network;", + AppCategory::Sports => "Education;Sports;", + AppCategory::Travel => "Education;", + AppCategory::Utility => "Utility;", + AppCategory::Video => "AudioVideo;Video;", + AppCategory::Weather => "Science;", + } + } + + /// Map an AppCategory to the closest LSApplicationCategoryType value that + /// matches that category. + pub fn macos_application_category_type(self) -> &'static str { + match &self { + AppCategory::Business => "public.app-category.business", + AppCategory::DeveloperTool => "public.app-category.developer-tools", + AppCategory::Education => "public.app-category.education", + AppCategory::Entertainment => "public.app-category.entertainment", + AppCategory::Finance => "public.app-category.finance", + AppCategory::Game => "public.app-category.games", + AppCategory::ActionGame => "public.app-category.action-games", + AppCategory::AdventureGame => "public.app-category.adventure-games", + AppCategory::ArcadeGame => "public.app-category.arcade-games", + AppCategory::BoardGame => "public.app-category.board-games", + AppCategory::CardGame => "public.app-category.card-games", + AppCategory::CasinoGame => "public.app-category.casino-games", + AppCategory::DiceGame => "public.app-category.dice-games", + AppCategory::EducationalGame => "public.app-category.educational-games", + AppCategory::FamilyGame => "public.app-category.family-games", + AppCategory::KidsGame => "public.app-category.kids-games", + AppCategory::MusicGame => "public.app-category.music-games", + AppCategory::PuzzleGame => "public.app-category.puzzle-games", + AppCategory::RacingGame => "public.app-category.racing-games", + AppCategory::RolePlayingGame => "public.app-category.role-playing-games", + AppCategory::SimulationGame => "public.app-category.simulation-games", + AppCategory::SportsGame => "public.app-category.sports-games", + AppCategory::StrategyGame => "public.app-category.strategy-games", + AppCategory::TriviaGame => "public.app-category.trivia-games", + AppCategory::WordGame => "public.app-category.word-games", + AppCategory::GraphicsAndDesign => "public.app-category.graphics-design", + AppCategory::HealthcareAndFitness => "public.app-category.healthcare-fitness", + AppCategory::Lifestyle => "public.app-category.lifestyle", + AppCategory::Medical => "public.app-category.medical", + AppCategory::Music => "public.app-category.music", + AppCategory::News => "public.app-category.news", + AppCategory::Photography => "public.app-category.photography", + AppCategory::Productivity => "public.app-category.productivity", + AppCategory::Reference => "public.app-category.reference", + AppCategory::SocialNetworking => "public.app-category.social-networking", + AppCategory::Sports => "public.app-category.sports", + AppCategory::Travel => "public.app-category.travel", + AppCategory::Utility => "public.app-category.utilities", + AppCategory::Video => "public.app-category.video", + AppCategory::Weather => "public.app-category.weather", + } + } +} + +impl<'d> serde::Deserialize<'d> for AppCategory { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_str(AppCategoryVisitor { did_you_mean: None }) + } +} + +struct AppCategoryVisitor { + did_you_mean: Option<&'static str>, +} + +impl<'d> serde::de::Visitor<'d> for AppCategoryVisitor { + type Value = AppCategory; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.did_you_mean { + Some(string) => write!( + formatter, + "a valid app category string (did you mean \"{}\"?)", + string + ), + None => write!(formatter, "a valid app category string"), + } + } + + fn visit_str(mut self, value: &str) -> Result { + match AppCategory::from_str(value) { + Ok(category) => Ok(category), + Err(did_you_mean) => { + self.did_you_mean = did_you_mean; + let unexp = serde::de::Unexpected::Str(value); + Err(serde::de::Error::invalid_value(unexp, &self)) + } + } + } +} + +const CATEGORY_STRINGS: &[(&str, AppCategory)] = &[ + ("actiongame", AppCategory::ActionGame), + ("actiongames", AppCategory::ActionGame), + ("adventuregame", AppCategory::AdventureGame), + ("adventuregames", AppCategory::AdventureGame), + ("arcadegame", AppCategory::ArcadeGame), + ("arcadegames", AppCategory::ArcadeGame), + ("boardgame", AppCategory::BoardGame), + ("boardgames", AppCategory::BoardGame), + ("business", AppCategory::Business), + ("cardgame", AppCategory::CardGame), + ("cardgames", AppCategory::CardGame), + ("casinogame", AppCategory::CasinoGame), + ("casinogames", AppCategory::CasinoGame), + ("developer", AppCategory::DeveloperTool), + ("developertool", AppCategory::DeveloperTool), + ("developertools", AppCategory::DeveloperTool), + ("development", AppCategory::DeveloperTool), + ("dicegame", AppCategory::DiceGame), + ("dicegames", AppCategory::DiceGame), + ("education", AppCategory::Education), + ("educationalgame", AppCategory::EducationalGame), + ("educationalgames", AppCategory::EducationalGame), + ("entertainment", AppCategory::Entertainment), + ("familygame", AppCategory::FamilyGame), + ("familygames", AppCategory::FamilyGame), + ("finance", AppCategory::Finance), + ("fitness", AppCategory::HealthcareAndFitness), + ("game", AppCategory::Game), + ("games", AppCategory::Game), + ("graphicdesign", AppCategory::GraphicsAndDesign), + ("graphicsanddesign", AppCategory::GraphicsAndDesign), + ("graphicsdesign", AppCategory::GraphicsAndDesign), + ("healthcareandfitness", AppCategory::HealthcareAndFitness), + ("healthcarefitness", AppCategory::HealthcareAndFitness), + ("kidsgame", AppCategory::KidsGame), + ("kidsgames", AppCategory::KidsGame), + ("lifestyle", AppCategory::Lifestyle), + ("logicgame", AppCategory::PuzzleGame), + ("medical", AppCategory::Medical), + ("medicalsoftware", AppCategory::Medical), + ("music", AppCategory::Music), + ("musicgame", AppCategory::MusicGame), + ("musicgames", AppCategory::MusicGame), + ("news", AppCategory::News), + ("photography", AppCategory::Photography), + ("productivity", AppCategory::Productivity), + ("puzzlegame", AppCategory::PuzzleGame), + ("puzzlegames", AppCategory::PuzzleGame), + ("racinggame", AppCategory::RacingGame), + ("racinggames", AppCategory::RacingGame), + ("reference", AppCategory::Reference), + ("roleplaying", AppCategory::RolePlayingGame), + ("roleplayinggame", AppCategory::RolePlayingGame), + ("roleplayinggames", AppCategory::RolePlayingGame), + ("rpg", AppCategory::RolePlayingGame), + ("simulationgame", AppCategory::SimulationGame), + ("simulationgames", AppCategory::SimulationGame), + ("socialnetwork", AppCategory::SocialNetworking), + ("socialnetworking", AppCategory::SocialNetworking), + ("sports", AppCategory::Sports), + ("sportsgame", AppCategory::SportsGame), + ("sportsgames", AppCategory::SportsGame), + ("strategygame", AppCategory::StrategyGame), + ("strategygames", AppCategory::StrategyGame), + ("travel", AppCategory::Travel), + ("triviagame", AppCategory::TriviaGame), + ("triviagames", AppCategory::TriviaGame), + ("utilities", AppCategory::Utility), + ("utility", AppCategory::Utility), + ("video", AppCategory::Video), + ("weather", AppCategory::Weather), + ("wordgame", AppCategory::WordGame), + ("wordgames", AppCategory::WordGame), +]; + +#[cfg(test)] +mod tests { + use super::AppCategory; + use std::str::FromStr; + + #[test] + fn category_from_string_ok() { + // Canonical name of category works: + assert_eq!( + AppCategory::from_str("Education"), + Ok(AppCategory::Education) + ); + assert_eq!( + AppCategory::from_str("Developer Tool"), + Ok(AppCategory::DeveloperTool) + ); + // Lowercase, spaces, and hyphens are fine: + assert_eq!( + AppCategory::from_str(" puzzle game "), + Ok(AppCategory::PuzzleGame) + ); + assert_eq!( + AppCategory::from_str("Role-playing game"), + Ok(AppCategory::RolePlayingGame) + ); + // Using macOS LSApplicationCategoryType value is fine: + assert_eq!( + AppCategory::from_str("public.app-category.developer-tools"), + Ok(AppCategory::DeveloperTool) + ); + assert_eq!( + AppCategory::from_str("public.app-category.role-playing-games"), + Ok(AppCategory::RolePlayingGame) + ); + // Using GNOME category name is fine: + assert_eq!( + AppCategory::from_str("Development"), + Ok(AppCategory::DeveloperTool) + ); + assert_eq!( + AppCategory::from_str("LogicGame"), + Ok(AppCategory::PuzzleGame) + ); + // Using common abbreviations is fine: + assert_eq!( + AppCategory::from_str("RPG"), + Ok(AppCategory::RolePlayingGame) + ); + } + + #[test] + fn category_from_string_did_you_mean() { + assert_eq!(AppCategory::from_str("gaming"), Err(Some("Game"))); + assert_eq!(AppCategory::from_str("photos"), Err(Some("Photography"))); + assert_eq!( + AppCategory::from_str("strategery"), + Err(Some("Strategy Game")) + ); + } + + #[test] + fn category_from_string_totally_wrong() { + assert_eq!(AppCategory::from_str("fhqwhgads"), Err(None)); + assert_eq!(AppCategory::from_str("WHARRGARBL"), Err(None)); + } + + #[test] + fn ls_application_category_type_round_trip() { + let values = &[ + "public.app-category.business", + "public.app-category.developer-tools", + "public.app-category.education", + "public.app-category.entertainment", + "public.app-category.finance", + "public.app-category.games", + "public.app-category.action-games", + "public.app-category.adventure-games", + "public.app-category.arcade-games", + "public.app-category.board-games", + "public.app-category.card-games", + "public.app-category.casino-games", + "public.app-category.dice-games", + "public.app-category.educational-games", + "public.app-category.family-games", + "public.app-category.kids-games", + "public.app-category.music-games", + "public.app-category.puzzle-games", + "public.app-category.racing-games", + "public.app-category.role-playing-games", + "public.app-category.simulation-games", + "public.app-category.sports-games", + "public.app-category.strategy-games", + "public.app-category.trivia-games", + "public.app-category.word-games", + "public.app-category.graphics-design", + "public.app-category.healthcare-fitness", + "public.app-category.lifestyle", + "public.app-category.medical", + "public.app-category.music", + "public.app-category.news", + "public.app-category.photography", + "public.app-category.productivity", + "public.app-category.reference", + "public.app-category.social-networking", + "public.app-category.sports", + "public.app-category.travel", + "public.app-category.utilities", + "public.app-category.video", + "public.app-category.weather", + ]; + // Test that if the user uses an LSApplicationCategoryType string as + // the category string, they will get back that same string for the + // macOS app bundle LSApplicationCategoryType. + for &value in values.iter() { + let category = AppCategory::from_str(value).expect(value); + assert_eq!(category.macos_application_category_type(), value); + } + } +} diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 1bd77e99..d10e5e1c 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -3,7 +3,10 @@ use std::{collections::HashMap, fmt::Display, path::PathBuf}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -/// The type of the package we're bundling. +mod category; +pub use category::AppCategory; + +/// The type of the package we're packaging. #[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, JsonSchema)] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[non_exhaustive] @@ -108,58 +111,6 @@ const ALL_PACKAGE_TYPES: &[PackageFormat] = &[ PackageFormat::AppImage, ]; -// TODO: Right now, these categories correspond to LSApplicationCategoryType -// values for OS X. There are also some additional GNOME registered categories -// that don't fit these; we should add those here too. -/// The possible app categories. -/// Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian. -#[allow(missing_docs)] -#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[non_exhaustive] -pub enum AppCategory { - Business, - DeveloperTool, - Education, - Entertainment, - Finance, - Game, - ActionGame, - AdventureGame, - ArcadeGame, - BoardGame, - CardGame, - CasinoGame, - DiceGame, - EducationalGame, - FamilyGame, - KidsGame, - MusicGame, - PuzzleGame, - RacingGame, - RolePlayingGame, - SimulationGame, - SportsGame, - StrategyGame, - TriviaGame, - WordGame, - GraphicsAndDesign, - HealthcareAndFitness, - Lifestyle, - Medical, - Music, - News, - Photography, - Productivity, - Reference, - SocialNetworking, - Sports, - Travel, - Utility, - Video, - Weather, -} - /// **macOS-only**. Corresponds to CFBundleTypeRole #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -206,9 +157,6 @@ pub struct FileAssociation { pub struct DebianConfig { /// the list of debian dependencies. pub depends: Option>, - /// List of custom files to add to the deb package. - /// Maps the path on the debian package to the path of the file to include (relative to the current working directory). - pub files: Option>, /// Path to a custom desktop file Handlebars template. /// /// Available variables: `categories`, `comment` (optional), `exec`, `icon` and `name`. diff --git a/crates/packager/Cargo.toml b/crates/packager/Cargo.toml index 1ab44228..f13f0063 100644 --- a/crates/packager/Cargo.toml +++ b/crates/packager/Cargo.toml @@ -45,3 +45,12 @@ regex = "1.9" [target."cfg(target_os = \"windows\")".dependencies.windows-sys] version = "0.48" features = ["Win32_System_SystemInformation", "Win32_System_Diagnostics_Debug"] + +[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\", target_os = \"openbsd\"))".dependencies] +image = "0.24" +md5 = "0.7" +heck = "0.4" +walkdir = "2.4" +ar = "0.9" +tar = "0.4" +libflate = "1.4" diff --git a/crates/packager/schema.json b/crates/packager/schema.json index 687b94ee..3860ebe9 100644 --- a/crates/packager/schema.json +++ b/crates/packager/schema.json @@ -339,7 +339,7 @@ ] }, "PackageFormat": { - "description": "The type of the package we're bundling.", + "description": "The type of the package we're packaging.", "oneOf": [ { "description": "The macOS application bundle (.app).", @@ -403,46 +403,46 @@ "description": "The possible app categories. Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian.", "type": "string", "enum": [ - "business", - "developerTool", - "education", - "entertainment", - "finance", - "game", - "actionGame", - "adventureGame", - "arcadeGame", - "boardGame", - "cardGame", - "casinoGame", - "diceGame", - "educationalGame", - "familyGame", - "kidsGame", - "musicGame", - "puzzleGame", - "racingGame", - "rolePlayingGame", - "simulationGame", - "sportsGame", - "strategyGame", - "triviaGame", - "wordGame", - "graphicsAndDesign", - "healthcareAndFitness", - "lifestyle", - "medical", - "music", - "news", - "photography", - "productivity", - "reference", - "socialNetworking", - "sports", - "travel", - "utility", - "video", - "weather" + "Business", + "DeveloperTool", + "Education", + "Entertainment", + "Finance", + "Game", + "ActionGame", + "AdventureGame", + "ArcadeGame", + "BoardGame", + "CardGame", + "CasinoGame", + "DiceGame", + "EducationalGame", + "FamilyGame", + "KidsGame", + "MusicGame", + "PuzzleGame", + "RacingGame", + "RolePlayingGame", + "SimulationGame", + "SportsGame", + "StrategyGame", + "TriviaGame", + "WordGame", + "GraphicsAndDesign", + "HealthcareAndFitness", + "Lifestyle", + "Medical", + "Music", + "News", + "Photography", + "Productivity", + "Reference", + "SocialNetworking", + "Sports", + "Travel", + "Utility", + "Video", + "Weather" ] }, "Binary": { @@ -600,16 +600,6 @@ "type": "string" } }, - "files": { - "description": "List of custom files to add to the deb package. Maps the path on the debian package to the path of the file to include (relative to the current working directory).", - "type": [ - "object", - "null" - ], - "additionalProperties": { - "type": "string" - } - }, "desktopTemplate": { "description": "Path to a custom desktop file Handlebars template.\n\nAvailable variables: `categories`, `comment` (optional), `exec`, `icon` and `name`.\n\nDefault file contents: ```text ```", "type": [ diff --git a/crates/packager/src/config.rs b/crates/packager/src/config.rs index 38ff0d18..d4ff5d2f 100644 --- a/crates/packager/src/config.rs +++ b/crates/packager/src/config.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; pub use cargo_packager_config::*; @@ -15,6 +15,8 @@ pub trait ConfigExt { fn nsis(&self) -> Option<&NsisConfig>; /// Returns the wix specific configuration fn wix(&self) -> Option<&WixConfig>; + /// Returns the debian specific configuration + fn deb(&self) -> Option<&DebianConfig>; /// Returns the architecture for the binary being packaged (e.g. "arm", "x86" or "x86_64"). fn target_arch(&self) -> crate::Result<&str>; /// Returns the path to the specified binary. @@ -38,6 +40,10 @@ impl ConfigExt for Config { self.wix.as_ref() } + fn deb(&self) -> Option<&DebianConfig> { + self.deb.as_ref() + } + fn target_arch(&self) -> crate::Result<&str> { Ok(if self.target_triple.starts_with("x86_64") { "x86_64" @@ -73,11 +79,30 @@ impl ConfigExt for Config { } pub(crate) trait ConfigExtInternal { + fn main_binary(&self) -> crate::Result<&Binary>; + fn main_binary_name(&self) -> crate::Result<&String>; fn resources(&self) -> Option>; fn find_ico(&self) -> Option; + fn copy_resources(&self, path: &Path) -> crate::Result<()>; + fn copy_binaries(&self, path: &Path) -> crate::Result<()>; } impl ConfigExtInternal for Config { + fn main_binary(&self) -> crate::Result<&Binary> { + self.binaries + .iter() + .find(|bin| bin.main) + .ok_or_else(|| crate::Error::MainBinaryNotFound) + } + + fn main_binary_name(&self) -> crate::Result<&String> { + self.binaries + .iter() + .find(|bin| bin.main) + .map(|b| &b.name) + .ok_or_else(|| crate::Error::MainBinaryNotFound) + } + fn resources(&self) -> Option> { self.resources.as_ref().map(|resources| { let mut out = Vec::new(); @@ -134,4 +159,33 @@ impl ConfigExtInternal for Config { }) .map(PathBuf::from) } + + fn copy_resources(&self, path: &Path) -> crate::Result<()> { + if let Some(resources) = self.resources() { + for resource in resources { + let dest = path.join(resource.target); + std::fs::create_dir_all(dest.parent().ok_or(crate::Error::ParentDirNotFound)?)?; + std::fs::copy(resource.src, dest)?; + } + } + Ok(()) + } + + fn copy_binaries(&self, path: &Path) -> crate::Result<()> { + if let Some(external_binaries) = &self.external_binaries { + for src in external_binaries { + let src = PathBuf::from(src); + let dest = path.join( + src.file_name() + .expect("failed to extract external binary filename") + .to_string_lossy() + .replace(&format!("-{}", self.target_triple), ""), + ); + std::fs::create_dir_all(dest.parent().ok_or(crate::Error::ParentDirNotFound)?)?; + std::fs::copy(src, dest)?; + } + } + + Ok(()) + } } diff --git a/crates/packager/src/deb/mod.rs b/crates/packager/src/deb/mod.rs index 2e280173..30ef3274 100644 --- a/crates/packager/src/deb/mod.rs +++ b/crates/packager/src/deb/mod.rs @@ -1,8 +1,356 @@ -use std::path::PathBuf; +use std::{ + collections::BTreeSet, + ffi::OsStr, + fs::File, + io::Write, + path::{Path, PathBuf}, +}; -use crate::config::Config; +use handlebars::Handlebars; +use heck::AsKebabCase; +use image::{codecs::png::PngDecoder, ImageDecoder}; +use serde::Serialize; +use walkdir::WalkDir; -pub fn package(_config: &Config) -> crate::Result> { - log::error!("`deb` format is not implemented yet!"); - std::process::exit(1); +use crate::{ + config::{Config, ConfigExt, ConfigExtInternal}, + util, +}; + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +pub struct DebIcon { + pub width: u32, + pub height: u32, + pub is_high_density: bool, + pub path: PathBuf, +} + +/// Generate the icon files and store them under the `data_dir`. +fn generate_icon_files(config: &Config, data_dir: &Path) -> crate::Result<()> { + let base_dir = data_dir.join("usr/share/icons/hicolor"); + let get_dest_path = |width: u32, height: u32, is_high_density: bool| { + base_dir.join(format!( + "{}x{}{}/apps/{}.png", + width, + height, + if is_high_density { "@2" } else { "" }, + config.main_binary_name().unwrap() + )) + }; + let mut icons_set = BTreeSet::new(); + if let Some(icons) = &config.icons { + for icon_path in icons { + let icon_path = PathBuf::from(icon_path); + if icon_path.extension() != Some(OsStr::new("png")) { + continue; + } + // Put file in scope so that it's closed when copying it + let deb_icon = { + let decoder = PngDecoder::new(File::open(&icon_path)?)?; + let width = decoder.dimensions().0; + let height = decoder.dimensions().1; + let is_high_density = util::is_retina(&icon_path); + let dest_path = get_dest_path(width, height, is_high_density); + DebIcon { + width, + height, + is_high_density, + path: dest_path, + } + }; + if !icons_set.contains(&deb_icon) { + std::fs::create_dir_all( + deb_icon + .path + .parent() + .ok_or(crate::Error::ParentDirNotFound)?, + )?; + std::fs::copy(&icon_path, &deb_icon.path)?; + icons_set.insert(deb_icon); + } + } + } + Ok(()) +} + +/// Generate the application desktop file and store it under the `data_dir`. +fn generate_desktop_file(config: &Config, data_dir: &Path) -> crate::Result<()> { + let bin_name = config.main_binary_name()?; + let desktop_file_name = format!("{}.desktop", bin_name); + let desktop_file_path = data_dir + .join("usr/share/applications") + .join(desktop_file_name); + + // For more information about the format of this file, see + // https://developer.gnome.org/integration-guide/stable/desktop-files.html.en + let file = &mut util::create_file(&desktop_file_path)?; + + let mut handlebars = Handlebars::new(); + handlebars.register_escape_fn(handlebars::no_escape); + if let Some(template) = config.deb().and_then(|d| d.desktop_template.as_ref()) { + handlebars + .register_template_string("main.desktop", std::fs::read_to_string(template)?) + .map_err(Box::new)?; + } else { + handlebars + .register_template_string("main.desktop", include_str!("./main.desktop")) + .map_err(Box::new)?; + } + + #[derive(Serialize)] + struct DesktopTemplateParams<'a> { + categories: &'a str, + comment: Option<&'a str>, + exec: &'a str, + icon: &'a str, + name: &'a str, + } + + handlebars.render_to_write( + "main.desktop", + &DesktopTemplateParams { + categories: config + .category + .map(|category| category.gnome_desktop_categories()) + .unwrap_or(""), + comment: config.description.as_deref(), + exec: bin_name, + icon: bin_name, + name: config.product_name.as_str(), + }, + file, + )?; + + Ok(()) +} + +pub fn generate_data(config: &Config, package_dir: &Path) -> crate::Result { + // Generate data files. + let data_dir = package_dir.join("data"); + let bin_dir = data_dir.join("usr/bin"); + + log::debug!("copying binaries"); + for bin in config.binaries.iter() { + let bin_path = config.binary_path(bin); + std::fs::create_dir_all(&bin_dir)?; + std::fs::copy(&bin_path, bin_dir.join(&bin.name))?; + } + + log::debug!("copying resource files"); + let resource_dir = data_dir.join("usr/lib").join(config.main_binary_name()?); + config.copy_resources(&resource_dir)?; + + log::debug!("copying external binaries"); + config.copy_binaries(&bin_dir)?; + + log::debug!("generating icons"); + generate_icon_files(config, &data_dir)?; + + log::debug!("generating desktop file"); + generate_desktop_file(config, &data_dir)?; + + Ok(data_dir) +} + +pub fn get_size>(path: P) -> crate::Result { + let mut result = 0; + let path = path.as_ref(); + + if path.is_dir() { + for entry in std::fs::read_dir(path)? { + let path = entry?.path(); + if path.is_file() { + result += path.metadata()?.len(); + } else { + result += get_size(path)?; + } + } + } else { + result = path.metadata()?.len(); + } + + Ok(result) +} + +/// Generates the debian control file and stores it under the `control_dir`. +fn generate_control_file( + config: &Config, + arch: &str, + control_dir: &Path, + data_dir: &Path, +) -> crate::Result<()> { + // For more information about the format of this file, see + // https://www.debian.org/doc/debian-policy/ch-controlfields.html + let dest_path = control_dir.join("control"); + let mut file = util::create_file(&dest_path)?; + writeln!(file, "Package: {}", AsKebabCase(&config.product_name))?; + writeln!(file, "Version: {}", &config.version)?; + writeln!(file, "Architecture: {}", arch)?; + // Installed-Size must be divided by 1024, see https://www.debian.org/doc/debian-policy/ch-controlfields.html#installed-size + writeln!(file, "Installed-Size: {}", get_size(data_dir)? / 1024)?; + let authors = config.authors.join(", "); + writeln!(file, "Maintainer: {}", authors)?; + if let Some(homepage) = &config.homepage { + writeln!(file, "Homepage: {}", homepage)?; + } + let dependencies = config + .deb() + .cloned() + .and_then(|d| d.depends) + .unwrap_or_default(); + if !dependencies.is_empty() { + writeln!(file, "Depends: {}", dependencies.join(", "))?; + } + + writeln!( + file, + "Description: {}", + config.description.as_deref().unwrap_or("(none)") + )?; + for line in config + .long_description + .as_deref() + .unwrap_or("(none)") + .lines() + { + let line = line.trim(); + if line.is_empty() { + writeln!(file, " .")?; + } else { + writeln!(file, " {}", line)?; + } + } + writeln!(file, "Priority: optional")?; + file.flush()?; + Ok(()) +} + +/// Create an `md5sums` file in the `control_dir` containing the MD5 checksums +/// for each file within the `data_dir`. +fn generate_md5sums(control_dir: &Path, data_dir: &Path) -> crate::Result<()> { + let md5sums_path = control_dir.join("md5sums"); + let mut md5sums_file = util::create_file(&md5sums_path)?; + for entry in WalkDir::new(data_dir) { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + continue; + } + let mut file = File::open(path)?; + let mut hash = md5::Context::new(); + std::io::copy(&mut file, &mut hash)?; + for byte in hash.compute().iter() { + write!(md5sums_file, "{:02x}", byte)?; + } + let rel_path = path.strip_prefix(data_dir)?; + let path_str = rel_path.to_str().ok_or_else(|| { + let msg = format!("Non-UTF-8 path: {:?}", rel_path); + std::io::Error::new(std::io::ErrorKind::InvalidData, msg) + })?; + writeln!(md5sums_file, " {}", path_str)?; + } + Ok(()) +} + +/// Writes a tar file to the given writer containing the given directory. +fn create_tar_from_dir, W: Write>(src_dir: P, dest_file: W) -> crate::Result { + let src_dir = src_dir.as_ref(); + let mut tar_builder = tar::Builder::new(dest_file); + for entry in WalkDir::new(src_dir) { + let entry = entry?; + let src_path = entry.path(); + if src_path == src_dir { + continue; + } + let dest_path = src_path.strip_prefix(src_dir)?; + if entry.file_type().is_dir() { + tar_builder.append_dir(dest_path, src_path)?; + } else { + let mut src_file = std::fs::File::open(src_path)?; + tar_builder.append_file(dest_path, &mut src_file)?; + } + } + let dest_file = tar_builder.into_inner()?; + Ok(dest_file) +} + +/// Creates a `.tar.gz` file from the given directory (placing the new file +/// within the given directory's parent directory), then deletes the original +/// directory and returns the path to the new file. +fn tar_and_gzip_dir>(src_dir: P) -> crate::Result { + let src_dir = src_dir.as_ref(); + let dest_path = src_dir.with_extension("tar.gz"); + let dest_file = util::create_file(&dest_path)?; + let gzip_encoder = libflate::gzip::Encoder::new(dest_file)?; + let gzip_encoder = create_tar_from_dir(src_dir, gzip_encoder)?; + let mut dest_file = gzip_encoder.finish().into_result()?; + dest_file.flush()?; + Ok(dest_path) +} + +/// Creates an `ar` archive from the given source files and writes it to the +/// given destination path. +fn create_archive(srcs: Vec, dest: &Path) -> crate::Result<()> { + let mut builder = ar::Builder::new(util::create_file(dest)?); + for path in &srcs { + builder.append_path(path)?; + } + builder.into_inner()?.flush()?; + Ok(()) +} + +pub fn package(config: &Config) -> crate::Result> { + let arch = match config.target_arch()? { + "x86" => "i386", + "x86_64" => "amd64", + // ARM64 is detected differently, armel isn't supported, so armhf is the only reasonable choice here. + "arm" => "armhf", + "aarch64" => "arm64", + other => other, + }; + + let package_base_name = format!("{}_{}_{}", config.main_binary_name()?, config.version, arch); + let package_name = format!("{package_base_name}.deb"); + + let base_dir = config.out_dir.join("deb"); + + let package_dir = base_dir.join(&package_base_name); + if package_dir.exists() { + std::fs::remove_dir_all(&package_dir)?; + } + let package_path = base_dir.join(&package_name); + + log::info!(action = "Pckaging"; "{} ({})", package_name, package_path.display()); + + log::debug!("generating data"); + let data_dir = generate_data(config, &package_dir)?; + + // Generate control files. + let control_dir = package_dir.join("control"); + log::debug!("generating control file"); + generate_control_file(config, arch, &control_dir, &data_dir)?; + log::debug!("generating md5sums"); + generate_md5sums(&control_dir, &data_dir)?; + + log::debug!("creating debian-binary file"); + // Generate `debian-binary` file; see + // http://www.tldp.org/HOWTO/Debian-Binary-Package-Building-HOWTO/x60.html#AEN66 + let debian_binary_path = package_dir.join("debian-binary"); + let mut file = util::create_file(&debian_binary_path)?; + file.write_all(b"2.0\n")?; + file.flush()?; + + // Apply tar/gzip/ar to create the final package file. + log::debug!("tar_and_gzip control dir"); + let control_tar_gz_path = tar_and_gzip_dir(control_dir)?; + + log::debug!("tar_and_gzip data dir"); + let data_tar_gz_path = tar_and_gzip_dir(data_dir)?; + + log::debug!("creating final archive: {}", package_path.display()); + create_archive( + vec![debian_binary_path, control_tar_gz_path, data_tar_gz_path], + &package_path, + )?; + Ok(vec![package_path]) } diff --git a/crates/packager/src/error.rs b/crates/packager/src/error.rs index 4d931b47..3dc71512 100644 --- a/crates/packager/src/error.rs +++ b/crates/packager/src/error.rs @@ -57,9 +57,12 @@ pub enum Error { /// Invalid app version when building [crate::PackageFormat::Msi] #[error("invalid app version: {0}")] InvalidAppVersion(String), + /// Handlebars render error. + #[error(transparent)] + HandleBarsRenderError(#[from] handlebars::RenderError), /// Handlebars template error. #[error(transparent)] - HandleBarsError(#[from] handlebars::RenderError), + HandleBarsTemplateError(#[from] Box), /// Nsis error #[error("error running makensis.exe: {0}")] NsisFailed(String), @@ -84,6 +87,29 @@ pub enum Error { /// Unsupported WiX language #[error("Language {0} not found. It must be one of {1}")] UnsupportedWixLanguage(String, String), + /// image crate errors. + #[error(transparent)] + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + ImageError(#[from] image::ImageError), + /// walkdir crate errors. + #[error(transparent)] + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + WalkDirError(#[from] walkdir::Error), + /// Path prefix strip error. + #[error(transparent)] + StripPrefixError(#[from] std::path::StripPrefixError), } /// Convenient type alias of Result type for cargo-packager. diff --git a/crates/packager/src/main.rs b/crates/packager/src/main.rs index 16be9e7d..23f918c9 100644 --- a/crates/packager/src/main.rs +++ b/crates/packager/src/main.rs @@ -15,8 +15,9 @@ fn load_configs_from_cwd(profile: &str, cli: &Cli) -> Result Result, + /// Specify the packages to build. + #[clap(short, long)] + packages: Option>, + /// Specify the manifest path to use for reading the configuration. + #[clap(long)] + manifest_path: Option, + /// Specify the package fromats to build. + #[clap(long, value_enum)] + formats: Option>, +} + fn try_run(cli: Cli) -> Result<()> { use std::fmt::Write; @@ -103,8 +134,10 @@ fn try_run(cli: Cli) -> Result<()> { "debug" }; - let mut packages = Vec::new(); + let mut outputs = Vec::new(); for (manifest_path, config) in load_configs_from_cwd(profile, &cli)? { + // change the directory to the manifest being built + // so paths are read relative to it std::env::set_current_dir( manifest_path .parent() @@ -112,15 +145,15 @@ fn try_run(cli: Cli) -> Result<()> { )?; // create the packages - packages.extend(package(&config)?); + outputs.extend(package(&config)?); } // print information when finished - let len = packages.len(); + let len = outputs.len(); if len >= 1 { let pluralised = if len == 1 { "package" } else { "packages" }; let mut printable_paths = String::new(); - for p in packages { + for p in outputs { for path in &p.paths { writeln!(printable_paths, " {}", util::display_path(path)).unwrap(); } @@ -131,74 +164,28 @@ fn try_run(cli: Cli) -> Result<()> { Ok(()) } -fn prettyprint_level(lvl: Level) -> &'static str { - match lvl { - Level::Error => "Error", - Level::Warn => "Warn", - Level::Info => "Info", - Level::Debug => "Debug", - Level::Trace => "Trace", - } -} - -fn format_error(err: clap::Error) -> clap::Error { - let mut app = I::command(); - err.format(&mut app) -} - -#[derive(Parser)] -#[clap( - author, - version, - about, - bin_name("cargo-packager"), - propagate_version(true), - no_binary_name(true) -)] -pub(crate) struct Cli { - /// Enables verbose logging - #[clap(short, long, global = true, action = ArgAction::Count)] - verbose: u8, - /// Package your app in release mode. - #[clap(short, long)] - release: bool, - /// Specify the cargo profile to use for packaging your app. - #[clap(long)] - profile: Option, - /// Specify the packages to build. - #[clap(short, long)] - packages: Option>, - /// Specify the manifest path to use for reading the configuration. - #[clap(long)] - manifest_path: Option, - /// Specify the package fromats to build. - #[clap(long, value_enum)] - formats: Option>, -} - fn main() { - let mut args = std::env::args_os(); - args.next(); + // prepare cli args + let args = std::env::args_os().skip(1); let cli = Cli::command(); let matches = cli.get_matches_from(args); - let res = Cli::from_arg_matches(&matches).map_err(format_error::); + let res = Cli::from_arg_matches(&matches).map_err(|e| e.format(&mut Cli::command())); let cli = match res { Ok(s) => s, Err(e) => e.exit(), }; + // setup logger + let filter_level = match cli.verbose { + 0 => Level::Info, + 1 => Level::Debug, + 2.. => Level::Trace, + } + .to_level_filter(); let mut builder = env_logger::Builder::from_default_env(); - let init_res = builder + let logger_init_res = builder .format_indent(Some(12)) - .filter( - None, - match cli.verbose { - 0 => Level::Info, - 1 => Level::Debug, - 2.. => Level::Trace, - } - .to_level_filter(), - ) + .filter(None, filter_level) .format(|f, record| { let mut is_command_output = false; if let Some(action) = record.key_values().get("action".into()) { @@ -207,24 +194,24 @@ fn main() { if !is_command_output { let mut action_style = f.style(); action_style.set_color(Color::Green).set_bold(true); - write!(f, "{:>12} ", action_style.value(action))?; } } else { let mut level_style = f.default_level_style(record.level()); level_style.set_bold(true); - - write!( - f, - "{:>12} ", - level_style.value(prettyprint_level(record.level())) - )?; + let level = match record.level() { + Level::Error => "Error", + Level::Warn => "Warn", + Level::Info => "Info", + Level::Debug => "Debug", + Level::Trace => "Trace", + }; + write!(f, "{:>12} ", level_style.value(level))?; } if !is_command_output && log_enabled!(Level::Debug) { let mut target_style = f.style(); target_style.set_color(Color::Black); - write!(f, "[{}] ", target_style.value(record.target()))?; } @@ -232,7 +219,7 @@ fn main() { }) .try_init(); - if let Err(err) = init_res { + if let Err(err) = logger_init_res { eprintln!("Failed to attach logger: {err}"); } diff --git a/crates/packager/src/nsis/mod.rs b/crates/packager/src/nsis/mod.rs index d3392de6..2b67984d 100644 --- a/crates/packager/src/nsis/mod.rs +++ b/crates/packager/src/nsis/mod.rs @@ -259,11 +259,7 @@ fn build_nsis_app_installer( #[cfg(target_os = "windows")] { - let main_binary = config - .binaries - .iter() - .find(|bin| bin.main) - .ok_or_else(|| crate::Error::MainBinaryNotFound)?; + let main_binary = config.main_binary()?; let app_exe_source = config.binary_path(main_binary); crate::sign::try_sign(&app_exe_source.with_extension("exe"), config)?; } diff --git a/crates/packager/src/util.rs b/crates/packager/src/util.rs index d18a039c..54360378 100644 --- a/crates/packager/src/util.rs +++ b/crates/packager/src/util.rs @@ -152,6 +152,7 @@ pub(crate) fn extract_zip(data: &[u8], path: &Path) -> crate::Result<()> { Ok(()) } +#[cfg(windows)] pub(crate) enum Bitness { X86_32, X86_64, @@ -194,3 +195,38 @@ pub(crate) fn log_if_needed(log_level: LogLevel, output: Output) { log::error!(action = action; "{}", String::from_utf8_lossy(output)) } } + +/// Returns true if the path has a filename indicating that it is a high-density +/// "retina" icon. Specifically, returns true the file stem ends with +/// "@2x" (a convention specified by the [Apple developer docs]( +/// https://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html)). +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +pub(crate) fn is_retina>(path: P) -> bool { + path.as_ref() + .file_stem() + .and_then(std::ffi::OsStr::to_str) + .map(|stem| stem.ends_with("@2x")) + .unwrap_or(false) +} + +/// Creates a new file at the given path, creating any parent directories as needed. +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +pub(crate) fn create_file(path: &Path) -> crate::Result> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let file = File::create(path)?; + Ok(std::io::BufWriter::new(file)) +} diff --git a/crates/packager/src/wix/mod.rs b/crates/packager/src/wix/mod.rs index 9cf922c9..b53c22d3 100644 --- a/crates/packager/src/wix/mod.rs +++ b/crates/packager/src/wix/mod.rs @@ -123,7 +123,7 @@ struct Binary { path: String, } -/// Generates the data required for the external binaries and extra binaries bundling. +/// Generates the data required for the external binaries. fn generate_binaries_data(config: &Config) -> crate::Result> { let mut binaries = Vec::new(); let cwd = std::env::current_dir()?; @@ -247,7 +247,7 @@ impl ResourceDirectory { /// Mapper between a resource directory name and its ResourceDirectory descriptor. type ResourceMap = BTreeMap; -/// Generates the data required for the resource bundling on wix +/// Generates the data required for the resource on wix fn generate_resource_data(config: &Config) -> crate::Result { let mut resources_map = ResourceMap::new(); if let Some(resources) = config.resources() { @@ -418,11 +418,7 @@ fn run_candle( extensions: Vec, log_level: LogLevel, ) -> crate::Result<()> { - let main_binary = config - .binaries - .iter() - .find(|bin| bin.main) - .ok_or_else(|| crate::Error::MainBinaryNotFound)?; + let main_binary = config.main_binary()?; let mut args = vec![ "-arch".to_string(), arch.to_string(), @@ -519,11 +515,7 @@ fn build_wix_app_installer( log::info!("Target: {}", arch); - let main_binary = config - .binaries - .iter() - .find(|bin| bin.main) - .ok_or_else(|| crate::Error::MainBinaryNotFound)?; + let main_binary = config.main_binary()?; let app_exe_source = config.binary_path(main_binary); sign::try_sign(&app_exe_source.with_extension("exe"), config)?; diff --git a/examples/dioxus/Cargo.toml b/examples/dioxus/Cargo.toml index 97960567..008c0634 100644 --- a/examples/dioxus/Cargo.toml +++ b/examples/dioxus/Cargo.toml @@ -7,6 +7,10 @@ edition = "2021" before-packaging-command = "dx build --platform desktop --release" product-name = "Dioxus example" identifier = "com.dioxus.example" +resources = ["src/**/*"] + +[package.metadata.packager.deb] +depends = ["libgtk-3-0", "libwebkit2gtk-4.1-0", "libayatana-appindicator3-1"] [dependencies] dioxus = "0.4" diff --git a/examples/tauri/Cargo.toml b/examples/tauri/Cargo.toml index 3f1744d7..0249c456 100644 --- a/examples/tauri/Cargo.toml +++ b/examples/tauri/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" before-packaging-command = "cargo tauri build" product-name = "Tauri example" identifier = "com.tauri.example" +resources = ["src/**/*"] icons = [ "icons/32x32.png", "icons/128x128.png", @@ -15,6 +16,10 @@ icons = [ "icons/icon.ico", ] +[package.metadata.packager.deb] +depends = ["libgtk-3-0", "libwebkit2gtk-4.1-0", "libayatana-appindicator3-1"] + + [build-dependencies] tauri-build = { version = "=2.0.0-alpha.7", features = [] }