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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
//! A library implementing an extended version of `cargo install`.
//!
//! The intended use of this crate is in an
//! [`xtask`](https://github.com/matklad/cargo-xtask)-style helper crate,
//! extending `cargo` with a `parcel` subcommand, providing a superset of the
//! functionality provided by `cargo install`. For such typical use, a single
//! call to the [`main`] function is sufficient to implement the helper crate's
//! main (and only) program.
//!
//! The rest of the API is provided for experimentation with alternative
//! interfaces to the functionality provided, but should be considered in flux;
//! no particular considerations to backward compatibility will be made,
//! although breaking API changes will result in a semver bump, following the
//! usual Rust conventions.

#![deny(missing_docs)]
#![warn(rust_2018_idioms)]

use std::{
    fmt, fs,
    path::{Path, PathBuf},
};

use pico_args::Arguments;

pub use anyhow::Error;

mod bundle;

mod config;
pub use config::Config;

mod install;
use install::InstallOpt;

mod util;

mod man;
pub use man::{ManSource, ManSourceFormat, ParseManSourceError};

/// A description of a parcel's metadata.
#[derive(Debug, Clone)]
pub struct Parcel {
    pkg_name: String,
    pkg_version: String,
    man_pages: Vec<ManSource>,
    pkg_data: Vec<PathBuf>,
    cargo_binaries: Vec<PathBuf>,
}

/// An action to be carried out.
///
/// Created from command-line arguments by `Parcel::action_from_env`.
#[derive(Debug)]
pub struct Action(ActionInner);

impl Action {
    fn help(text: &'static str) -> Action {
        Action(ActionInner::Help(text))
    }

    fn bundle(opt: bundle::Options) -> Action {
        Action(ActionInner::Bundle(Box::new(opt)))
    }

    fn install(opt: InstallOpt) -> Action {
        Action(ActionInner::Install(opt))
    }

    fn report(opt: InstallOpt) -> Action {
        Action(ActionInner::Report(opt))
    }

    fn uninstall(opt: InstallOpt) -> Action {
        Action(ActionInner::Uninstall(opt))
    }

    /// Execute the action.
    pub fn run(&self) -> Result<(), Error> {
        use self::ActionInner::*;
        match &self.0 {
            Help(text) => {
                println!("{}", text);
            }
            Bundle(opt) => bundle::create(opt)?,
            Install(opt) => opt.install()?,
            Report(opt) => opt.report(),
            Uninstall(opt) => opt.uninstall()?,
        }
        Ok(())
    }
}

#[derive(Debug)]
enum ActionInner {
    Install(InstallOpt),
    Report(InstallOpt),
    Uninstall(InstallOpt),
    Help(&'static str),
    Bundle(Box<bundle::Options>),
}

/// Describes an error caused by an invalid man page specification.
#[derive(Debug)]
pub struct ManPageError {
    source: String,
    error: Error,
}

impl fmt::Display for ManPageError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "malformed man page source file name '{}': {}",
            self.source, self.error
        )
    }
}

impl std::error::Error for ManPageError {}

/// Describes an error caused by a invalid pkg-data item.
#[derive(Debug)]
pub struct PkgDataError {
    item: String,
    error: Error,
}

impl fmt::Display for PkgDataError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "could not resolve package data item '{}': {}",
            self.item, self.error
        )
    }
}

impl std::error::Error for PkgDataError {}

/// A builder allowing to configuration of optional parcel properties.
// We can re-use `Parcel` here; this works since we always maintain a valid
// state.
#[derive(Debug)]
pub struct ParcelBuilder(Parcel);

impl ParcelBuilder {
    /// Configures the cargo binaries.
    pub fn cargo_binaries<I>(mut self, bins: I) -> Self
    where
        I: IntoIterator,
        I::Item: AsRef<str>,
    {
        for bin in bins {
            self.0.cargo_binaries.push(bin.as_ref().into())
        }
        self
    }

    /// Configures the man pages to install.
    pub fn man_pages<I>(mut self, sources: I) -> Result<Self, ManPageError>
    where
        I: IntoIterator,
        I::Item: AsRef<str>,
    {
        for source in sources {
            let source = source.as_ref();
            let parsed = source.parse::<ManSource>().map_err(|e| ManPageError {
                source: source.to_owned(),
                error: e.into(),
            })?;
            self.0.man_pages.push(parsed);
        }
        Ok(self)
    }

    /// Configures data files and directories to be installed in an
    /// application-specific directory.
    ///
    /// The strings passed will be expanded as glob expressions, and need to
    /// refer to existing files or directories, relative to the crate root
    /// directory. The resulting list of paths will be copied, recursively, in
    /// the case of directories into the application-specific data directory
    /// below the installation prefix. Note they will be placed in that
    /// directory using their file name, ignoring any leading directory
    /// components.
    pub fn pkg_data<I>(mut self, pkg_data: I) -> Result<Self, PkgDataError>
    where
        I: IntoIterator,
        I::Item: AsRef<str>,
    {
        for item in pkg_data {
            let paths = glob::glob_with(
                item.as_ref(),
                glob::MatchOptions {
                    case_sensitive: true,
                    require_literal_separator: true,
                    require_literal_leading_dot: true,
                },
            )
            .map_err(|e| PkgDataError {
                item: item.as_ref().to_owned(),
                error: e.into(),
            })?;
            for path in paths {
                let path = path.map_err(|e| PkgDataError {
                    item: item.as_ref().to_owned(),
                    error: e.into(),
                })?;
                self.0.pkg_data.push(path);
            }
        }
        Ok(self)
    }

    /// Finishes configuration and returns the parcel as configured.
    pub fn finish(self) -> Parcel {
        self.0
    }
}

impl Parcel {
    /// Create an empty parcel.
    ///
    /// The given name and version strings will be used to determine
    /// installation paths for resources.
    pub fn build<T: Into<String>, U: Into<String>>(pkg_name: T, version: U) -> ParcelBuilder {
        ParcelBuilder(Parcel {
            pkg_name: pkg_name.into(),
            pkg_version: version.into(),
            man_pages: Vec::new(),
            pkg_data: Vec::new(),
            cargo_binaries: Vec::new(),
        })
    }

    /// Returns the package name.
    pub fn pkg_name(&self) -> &str {
        &self.pkg_name
    }

    /// Returns the package version string.
    pub fn pkg_version(&self) -> &str {
        &self.pkg_version
    }

    /// Returns the man pages to be installed.
    pub fn pkg_data(&self) -> impl Iterator<Item = &Path> {
        self.pkg_data.iter().map(|s| s.as_ref())
    }

    /// Returns the cargo binaries to be installed.
    pub fn cargo_binaries(&self) -> impl Iterator<Item = &Path> {
        self.cargo_binaries.iter().map(|s| s.as_ref())
    }

    /// Returns the man pages to be installed.
    pub fn man_pages(&self) -> impl Iterator<Item = &ManSource> {
        self.man_pages.iter()
    }

    /// Constructs an action from the command line arguments.
    pub fn action_from_env(&self) -> Result<Action, Error> {
        let subcommand = match std::env::args_os().nth(1) {
            None => return Ok(Action::help(GLOBAL_HELP)),
            Some(s) => s,
        };
        let mut matches = Arguments::from_vec(std::env::args_os().skip(2).collect());
        let subcommand = &*subcommand.to_string_lossy();
        let action = match subcommand {
            "bundle" => {
                if matches.contains(["-h", "--help"]) {
                    Action::help(BUNDLE_HELP)
                } else {
                    let opt = bundle::Options::from_args(matches, self)?;
                    Action::bundle(opt)
                }
            }
            "install" => {
                if matches.contains(["-h", "--help"]) {
                    Action::help(INSTALL_HELP)
                } else {
                    let opt = InstallOpt::from_args(matches, self)?;
                    Action::install(opt)
                }
            }
            "report" => {
                if matches.contains(["-h", "--help"]) {
                    Action::help(REPORT_HELP)
                } else {
                    let opt = InstallOpt::from_args(matches, self)?;
                    Action::report(opt)
                }
            }
            "uninstall" => {
                if matches.contains(["-h", "--help"]) {
                    Action::help(UNINSTALL_HELP)
                } else {
                    let opt = InstallOpt::from_args(matches, self)?;
                    Action::uninstall(opt)
                }
            }
            "help" => {
                let free = matches.free()?;
                let help_text = match free.len() {
                    0 => GLOBAL_HELP,
                    1 => match free[0].as_str() {
                        "bundle" => BUNDLE_HELP,
                        "install" => INSTALL_HELP,
                        "uninstall" => UNINSTALL_HELP,
                        _ => return Err(UsageError.into()),
                    },
                    _ => return Err(UsageError.into()),
                };
                Action::help(help_text)
            }
            _ => return Err(UsageError.into()),
        };
        Ok(action)
    }
}

#[derive(Debug)]
struct UsageError;

impl fmt::Display for UsageError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(GLOBAL_HELP)
    }
}

impl std::error::Error for UsageError {}

fn run() -> Result<(), Error> {
    let manifest_contents = fs::read("Cargo.toml")?;
    let manifest: toml::value::Table = toml::from_slice(&manifest_contents)?;
    let config = Config::from_manifest(&manifest)?;
    let parcel = Parcel::build(config.package_name(), config.package_version())
        .cargo_binaries(config.cargo_binaries())
        .man_pages(config.man_pages())?
        .pkg_data(config.pkg_data())?
        .finish();
    let action = parcel.action_from_env()?;
    action.run()?;
    Ok(())
}

/// Run the command line interface.
///
/// This is the top-level entry point to the library's functionality, and the
/// only function needed inside a typical helper crate when using cargo-parcel
/// to provide an extended version of `cargo install` for a project.
pub fn main() -> ! {
    let rc = match run() {
        Ok(()) => 0,
        Err(e) => {
            eprintln!("{}", e);
            1
        }
    };
    std::process::exit(rc);
}

static GLOBAL_HELP: &str = r#"Extended cargo installer

USAGE:

    cargo parcel [SUBCOMMAND] [OPTIONS]

The available subcommands are:

    install      Install the parcel contents. Default prefix is ~/.local.
    uninstall    Uninstall the parcel contents.
    bundle       Create a distribution bundle.

See 'cargo parcel help <command> for more information on a specific command.
"#;

static BUNDLE_HELP: &str = r#"Create a binary distribution bundle

USAGE:

    cargo parcel bundle [OPTIONS]

OPTIONS:

    --verbose         Verbose operation.
    --prefix <DIR>    Installation prefix. Defaults to ~/.local.
    --root <DIR>      Top-level directory of the created archive. Defaults
                      to <NAME>-<VERSION>.
    -o <FILE>         Output file. The following extensions are
                      supported: ".tar", ".tar.gz", ".tar.bz2", ".tar.xz"
                      and ".tar.zstd". Defaults to <NAME>-<VERSION>.tar.gz.
                      If "-", an uncompressed TAR archive will be written to
                      standard output.
    --tar <CMD>       Command to invoke for GNU tar. Defaults to "tar".

Create a binary distribution bundle, either as TAR format archive on standard
output, or, when the "-o" option is given, as an archive with a format based on
the extension of the given file. The "--prefix" option is the same as for the
"install" command.

This command requires GNU tar. If GNU tar is not available as "tar", you can
specify an alternative, such "gtar" on BSD platforms, using the "--tar" option.
"#;

static INSTALL_HELP: &str = r#"Install a parcel

USAGE:

    cargo parcel install [OPTIONS]

OPTIONS:

    --verbose           Verbose operation.
    --prefix <DIR>      Installation prefix. This defaults to ~/.local.
    --target <TARGET>   Rust target triple to build for.
    --no-strip          Do not strip the binaries.
    --dest-dir <DIR>    Destination directory, will be prepended to the
                        installation prefix.

This will, after compiling the crate in release mode, install the parcel
contents as described by the "package.metadata.parcel" section in
"Cargo.toml". The files will be installed into "<DESTDIR>/<PREFIX>", where
DESTDIR and PREFIX are specified with the "--dest-dir" and "--prefix" arguments,
respectively. If "--dest-dir" is not given, it defaults to the root directory.

PREFIX should correspond to the final installation directory, and may be
compiled into the binaries. DESTDIR can be used to direct the install to a
staging area.
"#;

static UNINSTALL_HELP: &str = r#"Uninstall a parcel

USAGE:

    cargo parcel uninstall [OPTIONS]

OPTIONS:

    --verbose           Verbose operation.
    --prefix <DIR>      Installation prefix. This defaults to ~/.local.
    --dest-dir <DIR>    Destination directory, will be prepended to the
                        installation prefix.

This will uninstall the parcel contents installed by the "install" command. The
"--prefix" and "--dest-dir" arguments should be the same as given to the
"install" invocation that should be counteracted.
"#;

static REPORT_HELP: &str = "Report what would get installed";