#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#![doc = include_str!("../README.md")]
use std::{
error::Error,
ffi::OsStr,
fs, io,
os::unix::fs::DirBuilderExt,
path::{Path, PathBuf},
process,
};
#[cfg(feature = "mangen")]
use clap_mangen::Man;
#[cfg(feature = "complete")]
use {
clap_complete::{generate_to, shells},
clap_complete_nushell::Nushell,
};
#[cfg(feature = "icons")]
use {
resvg::tiny_skia::Transform,
resvg::usvg::{Options, Tree, TreeParsing},
};
pub struct Bootstrap {
name: String,
#[cfg(feature = "complete")]
cli: clap::Command,
outdir: PathBuf,
}
impl Bootstrap {
#[must_use]
pub fn new<P: AsRef<OsStr>>(
name: &str,
#[cfg(feature = "complete")] cli: clap::Command,
outdir: P,
) -> Self {
Self {
name: name.to_string(),
#[cfg(feature = "complete")]
cli,
outdir: Path::new(&outdir).to_path_buf(),
}
}
#[cfg(feature = "complete")]
fn gencomp(&self, gen: &str, outdir: &Path) -> Result<(), Box<dyn Error>> {
let mut cmd = self.cli.clone();
let path = match gen {
"bash" => generate_to(shells::Bash, &mut cmd, &self.name, outdir)?,
"fish" => generate_to(shells::Fish, &mut cmd, &self.name, outdir)?,
"nu" => generate_to(Nushell, &mut cmd, &self.name, outdir)?,
"pwsh" => generate_to(shells::PowerShell, &mut cmd, &self.name, outdir)?,
"zsh" => generate_to(shells::Zsh, &mut cmd, &self.name, outdir)?,
"elvish" => generate_to(shells::Elvish, &mut cmd, &self.name, outdir)?,
_ => unimplemented!(),
};
println!(" {}", path.display());
Ok(())
}
#[cfg(feature = "complete")]
pub fn completions(&self) -> Result<(), Box<dyn Error>> {
println!("Generating completions:");
["bash", "fish", "nu", "pwsh", "zsh", "elvish"]
.iter()
.try_for_each(|gen| {
let mut outdir = self.outdir.clone();
let base = match *gen {
"bash" => ["share", "bash-completion", "completions"],
"zsh" => ["share", "zsh", "site-functions"],
"nu" => ["share", "nu", "completions"],
"pwsh" => ["share", "pwsh", "completions"],
"fish" => ["share", "fish", "completions"],
"elvish" => ["share", "elvish", "completions"],
_ => unimplemented!(),
};
base.iter().for_each(|d| outdir.push(d));
let mut dirbuilder = fs::DirBuilder::new();
dirbuilder.recursive(true).mode(0o0755);
if !outdir.exists() {
dirbuilder.create(&outdir)?;
}
self.gencomp(gen, &outdir)
})?;
Ok(())
}
fn copy_bin(&self, target_dir: Option<String>) -> Result<(), Box<dyn Error>> {
println!("Copying binary:");
let mut bindir = self.outdir.clone();
bindir.push("bin");
let mut dirbuilder = fs::DirBuilder::new();
dirbuilder.recursive(true).mode(0o0755);
if !bindir.exists() {
dirbuilder.create(&bindir)?;
}
let mut outfile = bindir;
outfile.push(&self.name);
let infile: PathBuf = if let Some(target_dir) = target_dir {
[&target_dir, &self.name].iter().collect()
} else {
["target", "release", &self.name].iter().collect()
};
if !infile.exists() {
eprintln!("Error: you must run \"cargo build --release\" first");
}
fs::copy(&infile, &outfile)?;
println!(" {} -> {}", infile.display(), outfile.display());
Ok(())
}
fn compile_translation(&self, potfile: &str, lang: &str) -> Result<(), io::Error> {
let infile: PathBuf = ["po", potfile].iter().collect();
let mut lcdir = self.outdir.clone();
["share", "locale", lang, "LC_MESSAGES"]
.iter()
.for_each(|d| lcdir.push(d));
if !lcdir.exists() {
fs::create_dir_all(&lcdir)?;
}
let mut outfile = lcdir.clone();
outfile.push(&self.name);
outfile.set_extension("mo");
let output = process::Command::new("msgfmt")
.args([
infile.to_str().ok_or(io::Error::other("Bad path"))?,
"-o",
outfile.to_str().ok_or(io::Error::other("Bad path"))?,
])
.output()?;
if !output.status.success() {
process::exit(output.status.code().unwrap_or(1));
}
println!(" {} -> {}", infile.display(), outfile.display());
Ok(())
}
pub fn translations<P: AsRef<Path>>(&self, podir: P) -> Result<(), Box<dyn Error>> {
fs::read_dir(podir)?.try_for_each(|e| {
match e {
Err(e) => return Err(e),
Ok(entry) => {
if entry
.path()
.extension()
.ok_or(io::Error::other("Bad extension"))?
== "po"
{
let Some(lang) = entry
.file_name()
.to_str()
.ok_or(io::Error::other("PathError"))?
.strip_suffix(".po")
.map(ToString::to_string)
else {
return Err(io::Error::other("File path error"));
};
let path = entry
.path()
.to_str()
.ok_or(io::Error::other("Path error"))
.map(ToString::to_string)?;
self.compile_translation(&path, &lang)?;
}
}
}
Ok(())
})?;
Ok(())
}
#[cfg(feature = "mangen")]
pub fn manpage(&self, section: u8) -> Result<(), io::Error> {
let fname = format!("{}.{section}", &self.name);
println!("Generating manpage {fname}:");
let command = self.cli.clone();
let mut outdir = self.outdir.clone();
["share", "man", &format!("man{section}")]
.iter()
.for_each(|d| outdir.push(d));
let mut dirbuilder = fs::DirBuilder::new();
dirbuilder.recursive(true).mode(0o0755);
if !outdir.exists() {
dirbuilder.create(&outdir)?;
}
let mut outfile = outdir;
outfile.push(fname);
let man = Man::new(command);
let mut buffer: Vec<u8> = vec![];
man.render(&mut buffer)?;
fs::write(&outfile, buffer)?;
println!(" {}", outfile.display());
Ok(())
}
#[cfg(feature = "icons")]
fn png(&self, tree: &Tree, size: u32, source: &Path) -> Result<(), Box<dyn Error>> {
let transform = Transform::from_scale(1.0, 1.0);
let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new(size, size) else {
return Err(String::from("Error creating png").into());
};
let tree = resvg::Tree::from_usvg(tree);
tree.render(transform, &mut pixmap.as_mut());
let mut outdir = self.outdir.clone();
let sizedir = format!("{size}x{size}");
["share", "icons", "hicolor", &sizedir, "apps"]
.iter()
.for_each(|d| outdir.push(d));
let mut dirbuilder = fs::DirBuilder::new();
dirbuilder.recursive(true).mode(0o0755);
if !outdir.exists() {
dirbuilder.create(&outdir)?;
}
let mut outfile = outdir;
let fname = source
.file_stem()
.and_then(|x| x.to_str().map(|x| format!("{x}.png")))
.unwrap();
outfile.push(&fname);
println!(" {} -> {}", source.display(), outfile.display());
pixmap.save_png(outfile)?;
Ok(())
}
#[cfg(feature = "icons")]
pub fn icons<P: AsRef<OsStr>>(&self, source: Option<P>) -> Result<(), Box<dyn Error>> {
println!("Creating png icons from svg:");
let infile = if let Some(s) = source {
Path::new(&s).to_path_buf()
} else {
let mut p = ["data", &self.name].iter().collect::<PathBuf>();
p.set_extension("svg");
p
};
eprintln!("infile: {}", infile.display());
let data = fs::read(&infile)?;
let tree = Tree::from_data(&data, &Options::default())?;
for size in [256, 128, 64, 48, 32, 24, 16] {
self.png(&tree, size, &infile)?;
}
Ok(())
}
pub fn docfiles<P: AsRef<Path> + AsRef<OsStr>>(
&self,
files: &[P],
doc_subdir: &Path,
) -> Result<(), Box<dyn Error>> {
println!("Copying documentation");
let docdir: PathBuf = [&self.outdir, &"share".into(), &"doc".into(), doc_subdir]
.iter()
.collect();
if !docdir.exists() {
fs::create_dir_all(&docdir)?;
}
files.iter().try_for_each(|f| {
let infile = PathBuf::from(f);
let Some(filename) = infile.file_name().map(PathBuf::from) else {
return Err(io::Error::other("Bad path").into());
};
let outfile: PathBuf = [&docdir, &filename].iter().collect();
fs::copy(&infile, &outfile)?;
println!(" {} -> {}", infile.display(), outfile.display());
Ok::<(), Box<dyn Error>>(())
})
}
pub fn install(
&self,
target_dir: Option<String>,
#[cfg(feature = "mangen")] section: u8,
) -> Result<(), Box<dyn Error>> {
self.copy_bin(target_dir)?;
#[cfg(feature = "complete")]
self.completions()?;
#[cfg(feature = "mangen")]
self.manpage(section)?;
#[cfg(feature = "icons")]
{
let infile: Option<&str> = None;
self.icons(infile)?;
}
Ok(())
}
}