#![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};
#[derive(Debug, Clone)]
pub struct Parcel {
pkg_name: String,
pkg_version: String,
man_pages: Vec<ManSource>,
pkg_data: Vec<PathBuf>,
cargo_binaries: Vec<PathBuf>,
}
#[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))
}
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>),
}
#[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 {}
#[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 {}
#[derive(Debug)]
pub struct ParcelBuilder(Parcel);
impl ParcelBuilder {
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
}
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)
}
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)
}
pub fn finish(self) -> Parcel {
self.0
}
}
impl Parcel {
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(),
})
}
pub fn pkg_name(&self) -> &str {
&self.pkg_name
}
pub fn pkg_version(&self) -> &str {
&self.pkg_version
}
pub fn pkg_data(&self) -> impl Iterator<Item = &Path> {
self.pkg_data.iter().map(|s| s.as_ref())
}
pub fn cargo_binaries(&self) -> impl Iterator<Item = &Path> {
self.cargo_binaries.iter().map(|s| s.as_ref())
}
pub fn man_pages(&self) -> impl Iterator<Item = &ManSource> {
self.man_pages.iter()
}
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(())
}
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";