use std::{
ffi::OsStr,
fmt, fs, io,
path::{Component, Path, PathBuf},
};
use anyhow::{bail, Context, Error};
use pico_args::Arguments;
use crate::{
man::{ManSource, ManSourceFormat},
util::{cmd, empty_env, get_prefix_opt, DisplayArgs},
Parcel,
};
#[derive(Debug, Default)]
struct InstallStep {
create_dirs: Vec<PathBuf>,
copy_files: Vec<(PathBuf, PathBuf)>,
copy_resources: Vec<(PathBuf, PathBuf)>,
action: Option<BuildAction>,
}
impl InstallStep {
fn target_paths(&self) -> impl Iterator<Item = &Path> {
self.copy_files
.iter()
.map(|(_src, dst)| dst.as_ref())
.chain(self.copy_resources.iter().map(|(_src, dst)| dst.as_ref()))
}
fn install(&self, verbose: bool) -> Result<(), Error> {
for dir in &self.create_dirs {
fs::create_dir_all(dir)
.with_context(|| format!("could not directory {}", dir.display()))?;
}
for (from, to) in &self.copy_files {
fs::create_dir_all(to.parent().unwrap()).with_context(|| {
format!("could not create target directory for {}", to.display())
})?;
fs::copy(from, to).with_context(|| {
format!("could not copy file {} to {}", from.display(), to.display())
})?;
if verbose {
if let Some(action) = &self.action {
println!(
"{} copied from {} ({})",
to.display(),
from.display(),
action.describe_product()
);
} else {
println!("{} copied from {}", to.display(), from.display());
}
}
}
for (from, to) in &self.copy_resources {
copy_resource(from, to)?;
}
Ok(())
}
fn uninstall(&self, verbose: bool) -> Result<(), Error> {
for (_from, to) in &self.copy_files {
match fs::remove_file(to) {
Err(e) => eprintln!("could not remove file {}: {}", to.display(), e),
Ok(_) => {
if verbose {
if let Some(action) = &self.action {
println!(
"removed file {} ({})",
to.display(),
action.describe_product()
);
} else {
println!("removed file {}", to.display());
}
}
}
}
}
for (from, to) in &self.copy_resources {
delete_resource(to, from)
.with_context(|| format!("could not delete resource {}", to.display()))?;
}
Ok(())
}
}
#[derive(Debug)]
enum BuildAction {
CargoBinaries {
prefix: PathBuf,
target: Option<String>,
},
StripBinaries {
paths: Vec<(PathBuf, PathBuf)>,
},
PandocMan {
source: PathBuf,
output: PathBuf,
section: u8,
},
}
impl BuildAction {
fn describe_product(&self) -> BuildActionDescribeProduct<'_> {
BuildActionDescribeProduct { action: self }
}
fn execute(&self, verbose: bool) -> Result<(), Error> {
use BuildAction::*;
match self {
CargoBinaries { prefix, target } => {
let mut args = vec!["build", "--release"];
if let Some(target) = target {
args.extend(&["--target", target]);
}
cmd(
"cargo",
args,
vec![("PARCEL_INSTALL_PREFIX", &prefix)],
verbose,
)?;
}
StripBinaries { paths } => {
for (input, output) in paths {
fs::create_dir_all(output.parent().unwrap())?;
fs::copy(input, output)?;
}
cmd(
"strip",
paths.iter().map(|(_, output)| output),
empty_env(),
verbose,
)?;
}
PandocMan {
source,
output,
section,
} => {
let args = &[
"-s",
"-M",
&format!("section={}", section),
"-t",
"man",
"-o",
];
let extra_args = &[&output, &source];
let output_dir = output.parent().unwrap();
fs::create_dir_all(output_dir)?;
let args = args
.iter()
.map(|s| OsStr::new(s))
.chain(extra_args.iter().map(|p| p.as_os_str()));
cmd("pandoc", args, empty_env(), verbose)?;
}
}
Ok(())
}
}
impl<'a> fmt::Display for BuildAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BuildAction::CargoBinaries { prefix, target } => {
write!(
f,
"running cargo build ({}), passing prefix {}",
target.as_ref().map(|s| s.as_str()).unwrap_or("default"),
prefix.display()
)?;
}
BuildAction::StripBinaries { .. } => {
write!(f, "stripping binaries")?;
}
BuildAction::PandocMan { source, .. } => {
write!(f, "running pandoc for man page {}", source.display(),)?;
}
}
Ok(())
}
}
#[derive(Debug)]
struct BuildActionDescribeProduct<'a> {
action: &'a BuildAction,
}
impl<'a> fmt::Display for BuildActionDescribeProduct<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.action {
BuildAction::CargoBinaries { .. } => {
write!(f, "cargo binary")?;
}
BuildAction::StripBinaries { .. } => {
write!(f, "stripped cargo binary")?;
}
BuildAction::PandocMan { source, .. } => {
write!(f, "man page, rendered from {}", source.display())?;
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct InstallOpt {
verbose: bool,
strip: bool,
prefix: PathBuf,
destdir: Option<PathBuf>,
config: Parcel,
cargo_target: Option<String>,
}
impl InstallOpt {
pub fn new(config: Parcel, prefix: impl Into<PathBuf>) -> Self {
InstallOpt {
prefix: prefix.into(),
verbose: false,
strip: false,
destdir: None,
config,
cargo_target: None,
}
}
pub fn dest_dir(&mut self, dest_dir: impl Into<PathBuf>) -> &mut InstallOpt {
self.destdir = Some(dest_dir.into());
self
}
pub fn strip_binaries(&mut self, strip: bool) -> &mut InstallOpt {
self.strip = strip;
self
}
pub fn cargo_target<'a, T>(&mut self, target: T) -> &mut InstallOpt
where
T: Into<Option<&'a str>>,
{
self.cargo_target = target.into().map(ToOwned::to_owned);
self
}
pub fn from_args(mut args: Arguments, config: &Parcel) -> Result<Self, Error> {
let opt = InstallOpt {
prefix: get_prefix_opt(&mut args)?,
destdir: args.opt_value_from_str("--dest-dir")?,
config: config.clone(),
verbose: args.contains("--verbose"),
strip: !args.contains("--no-strip"),
cargo_target: args.opt_value_from_str("--target")?,
};
let remainder = args.finish();
if !remainder.is_empty() {
bail!("unprocessed arguments: {}", DisplayArgs(remainder.iter()))
}
Ok(opt)
}
fn push_man_dir(&self, mut path: PathBuf) -> PathBuf {
path.push("share");
path.push("man");
path
}
fn dest_prefix(&self) -> PathBuf {
if let Some(d) = &self.destdir {
let mut path = d.clone();
for c in self.prefix.components() {
match c {
Component::RootDir => {}
Component::Normal(c) => path.push(c),
_ => unimplemented!(), }
}
path
} else {
self.prefix.clone()
}
}
fn man_dest_dir(&self) -> PathBuf {
self.push_man_dir(self.dest_prefix())
}
fn output_dir(&self) -> PathBuf {
let mut path = PathBuf::new();
path.push("target");
path.push("parcel");
path
}
fn man_output(&self, name: &str) -> PathBuf {
let mut path = self.output_dir();
path.push("man");
path.push(name);
path
}
fn stripped_output(&self, name: &Path) -> PathBuf {
let mut path = self.output_dir();
path.push("stripped");
path.push(name);
path
}
fn man_dest(&self, source: &ManSource) -> PathBuf {
let mut path = self.man_dest_dir();
path.push(format!("man{}", source.section()));
path.push(source.target_file_name());
path
}
fn cargo_bin_source(&self, bin: &Path) -> PathBuf {
let mut path = PathBuf::new();
path.push("target");
if let Some(target) = &self.cargo_target {
path.push(target);
}
path.push("release");
path.push(bin);
path
}
fn push_bin_dir(&self, mut path: PathBuf) -> PathBuf {
path.push("bin");
path
}
fn bin_dest_dir(&self) -> PathBuf {
self.push_bin_dir(self.dest_prefix())
}
fn push_pkg_data_dir(&self, mut path: PathBuf) -> PathBuf {
for d in &["share", &self.config.pkg_name()] {
path.push(d);
}
path
}
fn pkg_data_dest_dir(&self) -> PathBuf {
self.push_pkg_data_dir(self.dest_prefix())
}
fn pkg_data_dest(&self, pkg_data: &Path) -> PathBuf {
let mut path = self.pkg_data_dest_dir();
path.push(pkg_data.file_name().unwrap());
path
}
fn bin_dest(&self, bin: &Path) -> PathBuf {
let mut path = self.bin_dest_dir();
path.push(bin);
path
}
fn cargo_build_action(&self) -> BuildAction {
BuildAction::CargoBinaries {
prefix: self.prefix.clone(),
target: self.cargo_target.clone(),
}
}
fn plan(&self) -> Vec<InstallStep> {
let mut steps = Vec::new();
if !self.config.cargo_binaries.is_empty() {
if self.strip {
let strip_files = self
.config
.cargo_binaries
.iter()
.map(|bin| (self.cargo_bin_source(bin), self.stripped_output(bin)))
.collect();
let copy_files = self
.config
.cargo_binaries
.iter()
.map(|bin| (self.stripped_output(bin), self.bin_dest(bin)))
.collect();
steps.push(InstallStep {
action: Some(self.cargo_build_action()),
..Default::default()
});
steps.push(InstallStep {
action: Some(BuildAction::StripBinaries { paths: strip_files }),
copy_files,
..Default::default()
});
} else {
let copy_files = self
.config
.cargo_binaries
.iter()
.map(|bin| (self.cargo_bin_source(bin), self.bin_dest(bin)))
.collect();
steps.push(InstallStep {
copy_files,
action: Some(self.cargo_build_action()),
..Default::default()
});
}
}
if !self.config.pkg_data.is_empty() {
let create_dirs = vec![self.pkg_data_dest_dir()];
let copy_resources = self
.config
.pkg_data
.iter()
.map(|pkg_data| (pkg_data.clone(), self.pkg_data_dest(pkg_data)))
.collect();
steps.push(InstallStep {
create_dirs,
copy_resources,
..Default::default()
});
}
for page in &self.config.man_pages {
let (source, action) = match page.format() {
ManSourceFormat::Markdown => {
let output = self.man_output(page.target_file_name());
(
output.clone(),
Some(BuildAction::PandocMan {
section: page.section(),
source: page.path().to_owned(),
output,
}),
)
}
ManSourceFormat::Verbatim => (page.path().to_owned(), None),
};
steps.push(InstallStep {
copy_files: vec![(source, self.man_dest(page))],
action,
..Default::default()
})
}
steps
}
pub fn report(&self) {
for step in self.plan() {
match &step.action {
Some(action) => {
for path in step.target_paths() {
println!("{} {}", path.display(), action.describe_product());
}
}
None => {
for (from, to) in &step.copy_files {
println!("{} copied from {}", to.display(), from.display());
}
for (from, to) in &step.copy_resources {
println!(
"{} copied recursively from {}",
to.display(),
from.display()
);
}
}
}
}
}
pub fn install(&self) -> Result<(), Error> {
let steps = self.plan();
let actions = steps.iter().filter_map(|step| step.action.as_ref());
for action in actions {
if self.verbose {
println!("Pre-install: {}", action);
}
action
.execute(self.verbose)
.with_context(|| format!("could not execute pre-install action \"{}\"", action))?;
}
for step in &steps {
step.install(self.verbose)?;
}
Ok(())
}
pub fn uninstall(&self) -> Result<(), Error> {
for step in self.plan() {
step.uninstall(self.verbose)?;
}
Ok(())
}
}
fn copy_resource(from: &Path, to: &Path) -> Result<(), io::Error> {
if from.is_dir() {
fs::create_dir_all(&to)?;
for entry in fs::read_dir(from)? {
let entry = entry?;
let target = to.join(entry.file_name());
copy_resource(&entry.path(), &target)?;
}
} else {
fs::copy(from, to)?;
}
Ok(())
}
fn delete_resource(target: &Path, source: &Path) -> Result<(), ResourceDeleteError> {
match (target.is_dir(), source.is_dir()) {
(false, false) => fs::remove_file(target)?,
(false, true) => return Err(ResourceDeleteError::ExpectedDirectory),
(true, false) => return Err(ResourceDeleteError::ExpectedFile),
(true, true) => {
for entry in fs::read_dir(source)? {
let entry = entry?;
let target = target.join(entry.file_name());
delete_resource(&target, &entry.path())?;
}
}
}
Ok(())
}
#[derive(Debug)]
enum ResourceDeleteError {
ExpectedDirectory,
ExpectedFile,
Io(io::Error),
}
impl fmt::Display for ResourceDeleteError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ResourceDeleteError::*;
match self {
ExpectedDirectory => write!(f, "expected directory, found file"),
ExpectedFile => write!(f, "expected file, found directory"),
Io(e) => write!(f, "{}", e),
}
}
}
impl std::error::Error for ResourceDeleteError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ResourceDeleteError::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for ResourceDeleteError {
fn from(e: io::Error) -> Self {
ResourceDeleteError::Io(e)
}
}