1#![deny(missing_docs, unsafe_code)]
17#![warn(rust_2018_idioms)]
18
19use std::{
20 fmt, fs,
21 path::{Path, PathBuf},
22};
23
24use pico_args::Arguments;
25
26pub use anyhow::Error;
27
28mod bundle;
29
30mod config;
31pub use config::Config;
32
33mod install;
34use install::InstallOpt;
35
36pub mod util;
37
38mod man;
39pub use man::{ManSource, ManSourceFormat, ParseManSourceError};
40
41#[derive(Debug, Clone)]
43pub struct Parcel {
44 pkg_name: String,
45 pkg_version: String,
46 man_pages: Vec<ManSource>,
47 pkg_data: Vec<PathBuf>,
48 cargo_binaries: Vec<PathBuf>,
49}
50
51#[derive(Debug)]
55pub struct Action(ActionInner);
56
57impl Action {
58 fn help(text: &'static str) -> Action {
59 Action(ActionInner::Help(text))
60 }
61
62 fn bundle(opt: bundle::Options) -> Action {
63 Action(ActionInner::Bundle(Box::new(opt)))
64 }
65
66 fn install(opt: InstallOpt) -> Action {
67 Action(ActionInner::Install(opt))
68 }
69
70 fn report(opt: InstallOpt) -> Action {
71 Action(ActionInner::Report(opt))
72 }
73
74 fn uninstall(opt: InstallOpt) -> Action {
75 Action(ActionInner::Uninstall(opt))
76 }
77
78 pub fn run(&self) -> Result<(), Error> {
80 use self::ActionInner::*;
81 match &self.0 {
82 Help(text) => {
83 println!("{}", text);
84 }
85 Bundle(opt) => bundle::create(opt)?,
86 Install(opt) => opt.install()?,
87 Report(opt) => opt.report(),
88 Uninstall(opt) => opt.uninstall()?,
89 }
90 Ok(())
91 }
92}
93
94#[derive(Debug)]
95enum ActionInner {
96 Install(InstallOpt),
97 Report(InstallOpt),
98 Uninstall(InstallOpt),
99 Help(&'static str),
100 Bundle(Box<bundle::Options>),
101}
102
103#[derive(Debug)]
105pub struct ManPageError {
106 source: String,
107 error: Error,
108}
109
110impl fmt::Display for ManPageError {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 write!(
113 f,
114 "malformed man page source file name '{}': {}",
115 self.source, self.error
116 )
117 }
118}
119
120impl std::error::Error for ManPageError {}
121
122#[derive(Debug)]
124pub struct PkgDataError {
125 item: String,
126 error: Error,
127}
128
129impl fmt::Display for PkgDataError {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 write!(
132 f,
133 "could not resolve package data item '{}': {}",
134 self.item, self.error
135 )
136 }
137}
138
139impl std::error::Error for PkgDataError {}
140
141#[derive(Debug)]
145pub struct ParcelBuilder(Parcel);
146
147impl ParcelBuilder {
148 pub fn cargo_binaries<I>(mut self, bins: I) -> Self
150 where
151 I: IntoIterator,
152 I::Item: AsRef<str>,
153 {
154 for bin in bins {
155 self.0.cargo_binaries.push(bin.as_ref().into())
156 }
157 self
158 }
159
160 pub fn man_pages<I>(mut self, sources: I) -> Result<Self, ManPageError>
162 where
163 I: IntoIterator,
164 I::Item: AsRef<str>,
165 {
166 for source in sources {
167 let source = source.as_ref();
168 let parsed = source.parse::<ManSource>().map_err(|e| ManPageError {
169 source: source.to_owned(),
170 error: e.into(),
171 })?;
172 self.0.man_pages.push(parsed);
173 }
174 Ok(self)
175 }
176
177 pub fn pkg_data<I>(mut self, pkg_data: I) -> Result<Self, PkgDataError>
188 where
189 I: IntoIterator,
190 I::Item: AsRef<str>,
191 {
192 for item in pkg_data {
193 let paths = glob::glob_with(
194 item.as_ref(),
195 glob::MatchOptions {
196 case_sensitive: true,
197 require_literal_separator: true,
198 require_literal_leading_dot: true,
199 },
200 )
201 .map_err(|e| PkgDataError {
202 item: item.as_ref().to_owned(),
203 error: e.into(),
204 })?;
205 for path in paths {
206 let path = path.map_err(|e| PkgDataError {
207 item: item.as_ref().to_owned(),
208 error: e.into(),
209 })?;
210 self.0.pkg_data.push(path);
211 }
212 }
213 Ok(self)
214 }
215
216 pub fn finish(self) -> Parcel {
218 self.0
219 }
220}
221
222impl Parcel {
223 pub fn build<T: Into<String>, U: Into<String>>(pkg_name: T, version: U) -> ParcelBuilder {
228 ParcelBuilder(Parcel {
229 pkg_name: pkg_name.into(),
230 pkg_version: version.into(),
231 man_pages: Vec::new(),
232 pkg_data: Vec::new(),
233 cargo_binaries: Vec::new(),
234 })
235 }
236
237 pub fn pkg_name(&self) -> &str {
239 &self.pkg_name
240 }
241
242 pub fn pkg_version(&self) -> &str {
244 &self.pkg_version
245 }
246
247 pub fn pkg_data(&self) -> impl Iterator<Item = &Path> {
249 self.pkg_data.iter().map(|s| s.as_ref())
250 }
251
252 pub fn cargo_binaries(&self) -> impl Iterator<Item = &Path> {
254 self.cargo_binaries.iter().map(|s| s.as_ref())
255 }
256
257 pub fn man_pages(&self) -> impl Iterator<Item = &ManSource> {
259 self.man_pages.iter()
260 }
261
262 pub fn action_from_env(&self) -> Result<Action, Error> {
264 let subcommand = match std::env::args_os().nth(1) {
265 None => return Ok(Action::help(GLOBAL_HELP)),
266 Some(s) => s,
267 };
268 let mut matches = Arguments::from_vec(std::env::args_os().skip(2).collect());
269 let subcommand = &*subcommand.to_string_lossy();
270 let action = match subcommand {
271 "bundle" => {
272 if matches.contains(["-h", "--help"]) {
273 Action::help(BUNDLE_HELP)
274 } else {
275 let opt = bundle::Options::from_args(matches, self)?;
276 Action::bundle(opt)
277 }
278 }
279 "install" => {
280 if matches.contains(["-h", "--help"]) {
281 Action::help(INSTALL_HELP)
282 } else {
283 let opt = InstallOpt::from_args(matches, self)?;
284 Action::install(opt)
285 }
286 }
287 "report" => {
288 if matches.contains(["-h", "--help"]) {
289 Action::help(REPORT_HELP)
290 } else {
291 let opt = InstallOpt::from_args(matches, self)?;
292 Action::report(opt)
293 }
294 }
295 "uninstall" => {
296 if matches.contains(["-h", "--help"]) {
297 Action::help(UNINSTALL_HELP)
298 } else {
299 let opt = InstallOpt::from_args(matches, self)?;
300 Action::uninstall(opt)
301 }
302 }
303 "help" => {
304 let help_text = match matches
305 .opt_free_from_os_str(|s| s.to_str().map(String::from).ok_or_else(|| UsageError))?
306 {
307 None => GLOBAL_HELP,
308 Some(cmd) => match cmd.as_str() {
309 "bundle" => BUNDLE_HELP,
310 "install" => INSTALL_HELP,
311 "uninstall" => UNINSTALL_HELP,
312 _ => return Err(UsageError.into()),
313 },
314 };
315 Action::help(help_text)
316 }
317 _ => return Err(UsageError.into()),
318 };
319 Ok(action)
320 }
321}
322
323#[derive(Debug)]
324struct UsageError;
325
326impl fmt::Display for UsageError {
327 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328 f.write_str(GLOBAL_HELP)
329 }
330}
331
332impl std::error::Error for UsageError {}
333
334fn run() -> Result<(), Error> {
335 let manifest_contents = fs::read("Cargo.toml")?;
336 let manifest: toml::value::Table = toml::from_slice(&manifest_contents)?;
337 let config = Config::from_manifest(&manifest)?;
338 let parcel = Parcel::build(config.package_name(), config.package_version())
339 .cargo_binaries(config.cargo_binaries())
340 .man_pages(config.man_pages())?
341 .pkg_data(config.pkg_data())?
342 .finish();
343 let action = parcel.action_from_env()?;
344 action.run()?;
345 Ok(())
346}
347
348pub fn main() -> ! {
354 let rc = match run() {
355 Ok(()) => 0,
356 Err(e) => {
357 eprintln!("{}", e);
358 1
359 }
360 };
361 std::process::exit(rc);
362}
363
364static GLOBAL_HELP: &str = r#"Extended cargo installer
365
366USAGE:
367
368 cargo parcel [SUBCOMMAND] [OPTIONS]
369
370The available subcommands are:
371
372 install Install the parcel contents. Default prefix is ~/.local.
373 uninstall Uninstall the parcel contents.
374 bundle Create a distribution bundle.
375
376See 'cargo parcel help <command> for more information on a specific command.
377"#;
378
379static BUNDLE_HELP: &str = r#"Create a binary distribution bundle
380
381USAGE:
382
383 cargo parcel bundle [OPTIONS]
384
385OPTIONS:
386
387 --verbose Verbose operation.
388 --prefix <DIR> Installation prefix. Defaults to ~/.local.
389 --root <DIR> Top-level directory of the created archive. Defaults
390 to <NAME>-<VERSION>.
391 -o <FILE> Output file. The following extensions are
392 supported: ".tar", ".tar.gz", ".tar.bz2", ".tar.xz"
393 and ".tar.zstd". Defaults to <NAME>-<VERSION>.tar.gz.
394 If "-", an uncompressed TAR archive will be written to
395 standard output.
396 --tar <CMD> Command to invoke for GNU tar. Defaults to "tar".
397
398Create a binary distribution bundle, either as TAR format archive on standard
399output, or, when the "-o" option is given, as an archive with a format based on
400the extension of the given file. The "--prefix" option is the same as for the
401"install" command.
402
403This command requires GNU tar. If GNU tar is not available as "tar", you can
404specify an alternative, such "gtar" on BSD platforms, using the "--tar" option.
405"#;
406
407static INSTALL_HELP: &str = r#"Install a parcel
408
409USAGE:
410
411 cargo parcel install [OPTIONS]
412
413OPTIONS:
414
415 --verbose Verbose operation.
416 --prefix <DIR> Installation prefix. This defaults to ~/.local.
417 --target <TARGET> Rust target triple to build for.
418 --no-strip Do not strip the binaries.
419 --dest-dir <DIR> Destination directory, will be prepended to the
420 installation prefix.
421
422This will, after compiling the crate in release mode, install the parcel
423contents as described by the "package.metadata.parcel" section in
424"Cargo.toml". The files will be installed into "<DESTDIR>/<PREFIX>", where
425DESTDIR and PREFIX are specified with the "--dest-dir" and "--prefix" arguments,
426respectively. If "--dest-dir" is not given, it defaults to the root directory.
427
428PREFIX should correspond to the final installation directory, and may be
429compiled into the binaries. DESTDIR can be used to direct the install to a
430staging area.
431"#;
432
433static UNINSTALL_HELP: &str = r#"Uninstall a parcel
434
435USAGE:
436
437 cargo parcel uninstall [OPTIONS]
438
439OPTIONS:
440
441 --verbose Verbose operation.
442 --prefix <DIR> Installation prefix. This defaults to ~/.local.
443 --dest-dir <DIR> Destination directory, will be prepended to the
444 installation prefix.
445
446This will uninstall the parcel contents installed by the "install" command. The
447"--prefix" and "--dest-dir" arguments should be the same as given to the
448"install" invocation that should be counteracted.
449"#;
450
451static REPORT_HELP: &str = "Report what would get installed";