1use 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 #[clap(short, long, global = true, action = ArgAction::Count)]
39 verbose: u8,
40 #[clap(short, long, global = true)]
42 quite: bool,
43
44 #[clap(short, long, value_enum, value_delimiter = ',')]
46 formats: Option<Vec<PackageFormat>>,
47 #[clap(short, long)]
53 config: Option<String>,
54 #[clap(short = 'k', long, env = "CARGO_PACKAGER_SIGN_PRIVATE_KEY")]
56 private_key: Option<String>,
57 #[clap(long, env = "CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD")]
59 password: Option<String>,
60 #[clap(short, long, value_delimiter = ',')]
62 pub(crate) packages: Option<Vec<String>>,
63 #[clap(short, long, alias = "out")]
67 out_dir: Option<PathBuf>,
68 #[clap(long)]
72 binaries_dir: Option<PathBuf>,
73 #[clap(short, long, group = "cargo-profile")]
76 release: bool,
77 #[clap(long, group = "cargo-profile")]
80 profile: Option<String>,
81 #[clap(long)]
84 manifest_path: Option<PathBuf>,
85 #[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 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 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 let mut packages = package(&config)?;
178
179 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 let outputs = outputs
190 .into_iter()
191 .flat_map(|o| o.paths)
192 .collect::<Vec<_>>();
193
194 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
228pub 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
240pub 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}