cargo_packager/
lib.rs

1// Copyright 2023-2023 CrabNebula Ltd.
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! [![cargo-packager splash](https://github.com/crabnebula-dev/cargo-packager/raw/main/.github/splash.png)](https://github.com/crabnebula-dev/cargo-packager)
6//!
7//! Executable packager, bundler and updater. A cli tool and library to generate installers or app bundles for your executables.
8//! It also comes with useful addons:
9//! - an [updater](https://docs.rs/cargo-packager-updater)
10//! - a [resource resolver](https://docs.rs/cargo-packager-resource-resolver)
11//!
12//! ### Supported packages
13//!
14//! - macOS
15//!   - DMG (.dmg)
16//!   - Bundle (.app)
17//! - Linux
18//!   - Debian package (.deb)
19//!   - AppImage (.AppImage)
20//!   - Pacman (.tar.gz and PKGBUILD)
21//! - Windows
22//!   - NSIS (.exe)
23//!   - MSI using WiX Toolset (.msi)
24//!
25//! ## CLI
26//!
27//! This crate is a cargo subcommand so you can install using:
28//!
29//! ```sh
30//! cargo install cargo-packager --locked
31//! ```
32//! You then need to configure your app so the cli can recognize it. Configuration can be done in `Packager.toml` or `packager.json` in your project or modify Cargo.toml and include this snippet:
33//!
34//! ```toml
35//! [package.metadata.packager]
36//! before-packaging-command = "cargo build --release"
37//! ```
38//!
39//! Once, you are done configuring your app, run:
40//!
41//! ```sh
42//! cargo packager --release
43//! ```
44//!
45//! ### Configuration
46//!
47//! By default, the packager reads its configuration from `Packager.toml` or `packager.json` if it exists, and from `package.metadata.packager` table in `Cargo.toml`.
48//! You can also specify a custom configuration using the `-c/--config` cli argument.
49//!
50//! For a full list of configuration options, see [Config].
51//!
52//! You could also use the [schema](./schema.json) file from GitHub to validate your configuration or have auto completions in your IDE.
53//!
54//! ### Building your application before packaging
55//!
56//! By default, the packager doesn't build your application, so if your app requires a compilation step, the packager has an option to specify a shell command to be executed before packaing your app, `beforePackagingCommand`.
57//!
58//! ### Cargo profiles
59//!
60//! By default, the packager looks for binaries built using the `debug` profile, if your `beforePackagingCommand` builds your app using `cargo build --release`, you will also need to
61//! run the packager in release mode `cargo packager --release`, otherwise, if you have a custom cargo profile, you will need to specify it using `--profile` cli arg `cargo packager --profile custom-release-profile`.
62//!
63//! ### Library
64//!
65//! This crate is also published to crates.io as a library that you can integrate into your tooling, just make sure to disable the default-feature flags.
66//!
67//! ```sh
68//! cargo add cargo-packager --no-default-features
69//! ```
70//!
71//! #### Feature flags
72//!
73//! - **`cli`**: Enables the cli specifc features and dependencies. Enabled by default.
74//! - **`tracing`**: Enables `tracing` crate integration.
75
76#![cfg_attr(doc_cfg, feature(doc_cfg))]
77#![deny(missing_docs)]
78
79use std::{io::Write, path::PathBuf};
80
81mod codesign;
82mod error;
83mod package;
84mod shell;
85mod util;
86
87#[cfg(feature = "cli")]
88#[cfg_attr(doc_cfg, doc(cfg(feature = "cli")))]
89pub mod cli;
90pub mod config;
91pub mod sign;
92
93pub use config::{Config, PackageFormat};
94pub use error::{Error, Result};
95use flate2::{write::GzEncoder, Compression};
96pub use sign::SigningConfig;
97
98pub use package::{package, PackageOutput};
99use util::PathExt;
100
101#[cfg(feature = "cli")]
102fn parse_log_level(verbose: u8) -> tracing::Level {
103    match verbose {
104        0 => tracing_subscriber::EnvFilter::builder()
105            .from_env_lossy()
106            .max_level_hint()
107            .and_then(|l| l.into_level())
108            .unwrap_or(tracing::Level::INFO),
109        1 => tracing::Level::DEBUG,
110        2.. => tracing::Level::TRACE,
111    }
112}
113
114/// Inits the tracing subscriber.
115#[cfg(feature = "cli")]
116#[cfg_attr(doc_cfg, doc(cfg(feature = "cli")))]
117pub fn init_tracing_subscriber(verbosity: u8) {
118    let level = parse_log_level(verbosity);
119
120    let debug = level == tracing::Level::DEBUG;
121    let tracing = level == tracing::Level::TRACE;
122
123    let subscriber = tracing_subscriber::fmt()
124        .with_ansi(std::io::IsTerminal::is_terminal(&std::io::stderr()))
125        .with_target(debug)
126        .with_line_number(tracing)
127        .with_file(tracing)
128        .with_max_level(level);
129
130    let formatter = tracing_subscriber::fmt::format()
131        .compact()
132        .with_target(debug)
133        .with_line_number(tracing)
134        .with_file(tracing);
135
136    if tracing {
137        subscriber
138            .event_format(TracingFormatter::WithTime(formatter))
139            .init();
140    } else {
141        subscriber
142            .without_time()
143            .event_format(TracingFormatter::WithoutTime(formatter.without_time()))
144            .init();
145    }
146}
147
148#[cfg(feature = "cli")]
149enum TracingFormatter {
150    WithoutTime(
151        tracing_subscriber::fmt::format::Format<tracing_subscriber::fmt::format::Compact, ()>,
152    ),
153    WithTime(tracing_subscriber::fmt::format::Format<tracing_subscriber::fmt::format::Compact>),
154}
155
156#[cfg(feature = "cli")]
157struct ShellFieldVisitor {
158    message: String,
159}
160
161#[cfg(feature = "cli")]
162impl tracing::field::Visit for ShellFieldVisitor {
163    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
164        if field.name() == "message" {
165            self.message = value.to_string();
166        }
167    }
168
169    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
170        if field.name() == "message" {
171            self.message = format!("{value:?}");
172        }
173    }
174}
175
176#[cfg(feature = "cli")]
177impl<S, N> tracing_subscriber::fmt::FormatEvent<S, N> for TracingFormatter
178where
179    S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
180    N: for<'a> tracing_subscriber::fmt::FormatFields<'a> + 'static,
181{
182    fn format_event(
183        &self,
184        ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>,
185        mut writer: tracing_subscriber::fmt::format::Writer<'_>,
186        event: &tracing::Event<'_>,
187    ) -> std::fmt::Result {
188        if event.fields().any(|f| f.name() == "shell") {
189            let mut visitor = ShellFieldVisitor { message: "".into() };
190            event.record(&mut visitor);
191            writeln!(writer, "{}", visitor.message)
192        } else {
193            match self {
194                TracingFormatter::WithoutTime(formatter) => {
195                    formatter.format_event(ctx, writer, event)
196                }
197                TracingFormatter::WithTime(formatter) => formatter.format_event(ctx, writer, event),
198            }
199        }
200    }
201}
202
203/// Sign the specified packages and return the signatures paths.
204///
205/// If `packages` contain a directory in the case of [`PackageFormat::App`]
206/// it will zip the directory before signing and appends it to `packages`.
207#[tracing::instrument(level = "trace")]
208pub fn sign_outputs(
209    config: &SigningConfig,
210    packages: &mut Vec<PackageOutput>,
211) -> crate::Result<Vec<PathBuf>> {
212    let mut signatures = Vec::new();
213    for package in packages {
214        for path in &package.paths.clone() {
215            let path = if path.is_dir() {
216                let zip = path.with_additional_extension("tar.gz");
217                let dest_file = util::create_file(&zip)?;
218                let gzip_encoder = GzEncoder::new(dest_file, Compression::default());
219                let writer = util::create_tar_from_dir(path, gzip_encoder)?;
220                let mut dest_file = writer.finish()?;
221                dest_file.flush()?;
222
223                package.paths.push(zip);
224                package.paths.last().unwrap()
225            } else {
226                path
227            };
228            signatures.push(sign::sign_file(config, path)?);
229        }
230    }
231
232    Ok(signatures)
233}
234
235/// Package an app using the specified config.
236/// Then signs the generated packages.
237///
238/// This is similar to calling `sign_outputs(signing_config, package(config)?)`
239///
240/// Returns a tuple of list of packages and list of signatures.
241#[tracing::instrument(level = "trace")]
242pub fn package_and_sign(
243    config: &Config,
244    signing_config: &SigningConfig,
245) -> crate::Result<(Vec<PackageOutput>, Vec<PathBuf>)> {
246    let mut packages = package(config)?;
247    let signatures = sign_outputs(signing_config, &mut packages)?;
248    Ok((packages, signatures))
249}