1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
// Copyright 2023-2023 CrabNebula Ltd.
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

//! [![cargo-packager splash](https://github.com/crabnebula-dev/cargo-packager/raw/main/.github/splash.png)](https://github.com/crabnebula-dev/cargo-packager)
//!
//! Executable packager, bundler and updater. A cli tool and library to generate installers or app bundles for your executables.
//! It also comes with useful addons:
//! - an [updater](https://docs.rs/cargo-packager-updater)
//! - a [resource resolver](https://docs.rs/cargo-packager-resource-resolver)
//!
//! ### Supported packages
//!
//! - macOS
//!   - DMG (.dmg)
//!   - Bundle (.app)
//! - Linux
//!   - Debian package (.deb)
//!   - AppImage (.AppImage)
//!   - Pacman (.tar.gz and PKGBUILD)
//! - Windows
//!   - NSIS (.exe)
//!   - MSI using WiX Toolset (.msi)
//!
//! ## CLI
//!
//! This crate is a cargo subcommand so you can install using:
//!
//! ```sh
//! cargo install cargo-packager --locked
//! ```
//! 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:
//!
//! ```toml
//! [package.metadata.packager]
//! before-packaging-command = "cargo build --release"
//! ```
//!
//! Once, you are done configuring your app, run:
//!
//! ```sh
//! cargo packager --release
//! ```
//!
//! ### Configuration
//!
//! 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`.
//! You can also specify a custom configuration using the `-c/--config` cli argument.
//!
//! For a full list of configuration options, see [Config].
//!
//! You could also use the [schema](./schema.json) file from GitHub to validate your configuration or have auto completions in your IDE.
//!
//! ### Building your application before packaging
//!
//! 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`.
//!
//! ### Cargo profiles
//!
//! 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
//! 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`.
//!
//! ### Library
//!
//! 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.
//!
//! ```sh
//! cargo add cargo-packager --no-default-features
//! ```
//!
//! #### Feature flags
//!
//! - **`cli`**: Enables the cli specifc features and dependencies. Enabled by default.
//! - **`tracing`**: Enables `tracing` crate integration.

#![cfg_attr(doc_cfg, feature(doc_cfg))]
#![deny(missing_docs)]

use std::{io::Write, path::PathBuf};

mod codesign;
mod error;
mod package;
mod shell;
mod util;

#[cfg(feature = "cli")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "cli")))]
pub mod cli;
pub mod config;
pub mod sign;

pub use config::{Config, PackageFormat};
pub use error::{Error, Result};
use flate2::{write::GzEncoder, Compression};
pub use sign::SigningConfig;

pub use package::{package, PackageOuput};
use util::PathExt;

fn parse_log_level(verbose: u8) -> tracing::Level {
    match verbose {
        0 => tracing_subscriber::EnvFilter::builder()
            .from_env_lossy()
            .max_level_hint()
            .and_then(|l| l.into_level())
            .unwrap_or(tracing::Level::INFO),
        1 => tracing::Level::DEBUG,
        2.. => tracing::Level::TRACE,
    }
}

/// Inits the tracing subscriber.
#[cfg(feature = "cli")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "cli")))]
pub fn init_tracing_subscriber(verbosity: u8) {
    let level = parse_log_level(verbosity);

    let debug = level == tracing::Level::DEBUG;
    let tracing = level == tracing::Level::TRACE;

    tracing_subscriber::fmt()
        .with_ansi(std::io::IsTerminal::is_terminal(&std::io::stderr()))
        .without_time()
        .with_target(debug)
        .with_line_number(tracing)
        .with_file(tracing)
        .with_max_level(level)
        .init();
}

/// Sign the specified packages and return the signatures paths.
///
/// If `packages` contain a directory in the case of [`PackageFormat::App`]
/// it will zip the directory before signing and appends it to `packages`.
#[tracing::instrument(level = "trace")]
pub fn sign_outputs(
    config: &SigningConfig,
    packages: &mut Vec<PackageOuput>,
) -> crate::Result<Vec<PathBuf>> {
    let mut signatures = Vec::new();
    for package in packages {
        for path in &package.paths.clone() {
            let path = if path.is_dir() {
                let zip = path.with_additional_extension("tar.gz");
                let dest_file = util::create_file(&zip)?;
                let gzip_encoder = GzEncoder::new(dest_file, Compression::default());
                let writer = util::create_tar_from_dir(path, gzip_encoder)?;
                let mut dest_file = writer.finish()?;
                dest_file.flush()?;

                package.paths.push(zip);
                package.paths.last().unwrap()
            } else {
                path
            };
            signatures.push(sign::sign_file(config, path)?);
        }
    }

    Ok(signatures)
}

/// Package an app using the specified config.
/// Then signs the generated packages.
///
/// This is similar to calling `sign_outputs(signing_config, package(config)?)`
///
/// Returns a tuple of list of packages and list of signatures.
#[tracing::instrument(level = "trace")]
pub fn package_and_sign(
    config: &Config,
    signing_config: &SigningConfig,
) -> crate::Result<(Vec<PackageOuput>, Vec<PathBuf>)> {
    let mut packages = package(config)?;
    let signatures = sign_outputs(signing_config, &mut packages)?;
    Ok((packages, signatures))
}