Skip to main content

cargo_packager/cli/
mod.rs

1// Copyright 2023-2023 CrabNebula Ltd.
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! The cli entry point
6
7use std::{ffi::OsString, fmt::Write, fs, path::PathBuf};
8
9use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand};
10
11use crate::{
12    config::{LogLevel, PackageFormat},
13    init_tracing_subscriber, package, parse_log_level, sign_outputs, util, SigningConfig,
14};
15
16mod config;
17mod error;
18mod signer;
19
20use self::error::{Error, Result};
21
22#[derive(Debug, Clone, Subcommand)]
23enum Commands {
24    Signer(signer::Options),
25}
26
27#[derive(Parser, Debug)]
28#[clap(
29    author,
30    version,
31    about,
32    bin_name("cargo-packager"),
33    propagate_version(true),
34    no_binary_name(true)
35)]
36pub(crate) struct Cli {
37    /// Enables verbose logging.
38    #[clap(short, long, global = true, action = ArgAction::Count)]
39    verbose: u8,
40    /// Disables logging
41    #[clap(short, long, global = true)]
42    quite: bool,
43
44    /// The package fromats to build.
45    #[clap(short, long, value_enum, value_delimiter = ',')]
46    formats: Option<Vec<PackageFormat>>,
47    /// A configuration to read, which could be a JSON file,
48    /// TOML file, or a raw JSON string.
49    ///
50    /// By default, cargo-packager looks for `{p,P}ackager.{toml,json}` and
51    /// `[package.metadata.packager]` in `Cargo.toml` files.
52    #[clap(short, long)]
53    config: Option<String>,
54    /// Load a private key from a file or a string to sign the generated ouptuts.
55    #[clap(short = 'k', long, env = "CARGO_PACKAGER_SIGN_PRIVATE_KEY")]
56    private_key: Option<String>,
57    /// The password for the signing private key.
58    #[clap(long, env = "CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD")]
59    password: Option<String>,
60    /// Which packages to use from the current workspace.
61    #[clap(short, long, value_delimiter = ',')]
62    pub(crate) packages: Option<Vec<String>>,
63    /// The directory where the packages will be placed.
64    ///
65    /// If [`Config::binaries_dir`] is not defined, it is also the path where the binaries are located if they use relative paths.
66    #[clap(short, long, alias = "out")]
67    out_dir: Option<PathBuf>,
68    /// The directory where the [`Config::binaries`] exist.
69    ///
70    /// Defaults to [`Config::out_dir`]
71    #[clap(long)]
72    binaries_dir: Option<PathBuf>,
73    /// Package the release version of your app.
74    /// Ignored when `--config` is used.
75    #[clap(short, long, group = "cargo-profile")]
76    release: bool,
77    /// Cargo profile to use for packaging your app.
78    /// Ignored when `--config` is used.
79    #[clap(long, group = "cargo-profile")]
80    profile: Option<String>,
81    /// Path to Cargo.toml manifest path to use for reading the configuration.
82    /// Ignored when `--config` is used.
83    #[clap(long)]
84    manifest_path: Option<PathBuf>,
85    /// Target triple to use for detecting your app binaries.
86    #[clap(long)]
87    target: Option<String>,
88
89    #[command(subcommand)]
90    command: Option<Commands>,
91}
92
93#[tracing::instrument(level = "trace", skip(cli))]
94fn run_cli(cli: Cli) -> Result<()> {
95    tracing::trace!(cli= ?cli);
96
97    // run subcommand and exit if one was specified,
98    // otherwise run the default packaging command
99    if let Some(command) = cli.command {
100        match command {
101            Commands::Signer(opts) => signer::command(opts)?,
102        }
103        return Ok(());
104    }
105
106    let configs = config::detect_configs(&cli)?;
107
108    if configs.is_empty() {
109        tracing::error!("Couldn't detect a valid configuration file or all configurations are disabled! Nothing to do here.");
110        std::process::exit(1);
111    }
112
113    let cli_out_dir = cli
114        .out_dir
115        .as_ref()
116        .map(|p| {
117            if p.exists() {
118                dunce::canonicalize(p).map_err(|e| Error::IoWithPath(p.clone(), e))
119            } else {
120                fs::create_dir_all(p).map_err(|e| Error::IoWithPath(p.clone(), e))?;
121                Ok(p.to_owned())
122            }
123        })
124        .transpose()?;
125
126    let private_key = match cli.private_key {
127        Some(path) if PathBuf::from(&path).exists() => Some(
128            fs::read_to_string(&path).map_err(|e| Error::IoWithPath(PathBuf::from(&path), e))?,
129        ),
130        k => k,
131    };
132
133    let signing_config = private_key.map(|k| SigningConfig {
134        private_key: k,
135        password: cli.password,
136    });
137
138    let mut outputs = Vec::new();
139    let mut signatures = Vec::new();
140    for (config_dir, mut config) in configs {
141        tracing::trace!(config = ?config);
142
143        if let Some(dir) = &cli_out_dir {
144            config.out_dir.clone_from(dir)
145        }
146
147        if let Some(formats) = &cli.formats {
148            config.formats.replace(formats.clone());
149        }
150
151        if let Some(target_triple) = &cli.target {
152            config.target_triple.replace(target_triple.clone());
153        }
154
155        if config.log_level.is_none() && !cli.quite {
156            let level = match parse_log_level(cli.verbose) {
157                tracing::Level::ERROR => LogLevel::Error,
158                tracing::Level::WARN => LogLevel::Warn,
159                tracing::Level::INFO => LogLevel::Info,
160                tracing::Level::DEBUG => LogLevel::Debug,
161                tracing::Level::TRACE => LogLevel::Trace,
162            };
163            config.log_level.replace(level);
164        }
165
166        if let Some(path) = config_dir {
167            // change the directory to the config being built
168            // so paths will be read relative to it
169            let parent = path
170                .parent()
171                .ok_or_else(|| crate::Error::ParentDirNotFound(path.clone()))?;
172            std::env::set_current_dir(parent)
173                .map_err(|e| Error::IoWithPath(parent.to_path_buf(), e))?;
174        }
175
176        // create the packages
177        let mut packages = package(&config)?;
178
179        // sign the packages
180        if let Some(signing_config) = &signing_config {
181            let s = sign_outputs(signing_config, &mut packages)?;
182            signatures.extend(s);
183        }
184
185        outputs.extend(packages);
186    }
187
188    // flatten paths
189    let outputs = outputs
190        .into_iter()
191        .flat_map(|o| o.paths)
192        .collect::<Vec<_>>();
193
194    // print information when finished
195    let len = outputs.len();
196    if len >= 1 {
197        let pluralised = if len == 1 { "package" } else { "packages" };
198        let mut printable_paths = String::new();
199        for path in outputs {
200            let _ = writeln!(printable_paths, "        {}", util::display_path(path));
201        }
202        tracing::info!(
203            "Finished packaging {} {} at:\n{}",
204            len,
205            pluralised,
206            printable_paths
207        );
208    }
209
210    let len = signatures.len();
211    if len >= 1 {
212        let pluralised = if len == 1 { "signature" } else { "signatures" };
213        let mut printable_paths = String::new();
214        for path in signatures {
215            let _ = writeln!(printable_paths, "        {}", util::display_path(path));
216        }
217        tracing::info!(
218            "Finished signing packages, {} {} at:\n{}",
219            len,
220            pluralised,
221            printable_paths
222        );
223    }
224
225    Ok(())
226}
227
228/// Run the packager CLI
229pub fn run<I, A>(args: I, bin_name: Option<String>)
230where
231    I: IntoIterator<Item = A>,
232    A: Into<OsString> + Clone,
233{
234    if let Err(e) = try_run(args, bin_name) {
235        tracing::error!("{}", e);
236        std::process::exit(1);
237    }
238}
239
240/// Try run the packager CLI
241pub fn try_run<I, A>(args: I, bin_name: Option<String>) -> Result<()>
242where
243    I: IntoIterator<Item = A>,
244    A: Into<OsString> + Clone,
245{
246    let cli = match &bin_name {
247        Some(bin_name) => Cli::command().bin_name(bin_name),
248        None => Cli::command(),
249    };
250    let matches = cli.get_matches_from(args);
251    let cli = Cli::from_arg_matches(&matches).map_err(|e| {
252        e.format(&mut match &bin_name {
253            Some(bin_name) => Cli::command().bin_name(bin_name),
254            None => Cli::command(),
255        })
256    })?;
257
258    if !cli.quite {
259        init_tracing_subscriber(cli.verbose);
260        if std::env::var_os("CARGO_TERM_COLOR").is_none() {
261            std::env::set_var("CARGO_TERM_COLOR", "always");
262        }
263    }
264
265    run_cli(cli)
266}