1#![warn(clippy::all, clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![doc = include_str!("../README.md")]
4
5use std::{
6 error::Error,
7 ffi::OsStr,
8 fs, io,
9 os::unix::fs::DirBuilderExt,
10 path::{Path, PathBuf},
11 process,
12};
13
14#[cfg(feature = "mangen")]
15use clap_mangen::Man;
16
17#[cfg(feature = "complete")]
18use {
19 clap_complete::{generate_to, shells},
20 clap_complete_nushell::Nushell,
21};
22
23#[cfg(feature = "icons")]
24use {
25 resvg::tiny_skia::Transform,
26 resvg::usvg::{Options, Tree, TreeParsing},
27};
28
29pub struct Bootstrap {
31 name: String,
32 #[cfg(feature = "complete")]
33 cli: clap::Command,
34 outdir: PathBuf,
35}
36
37impl Bootstrap {
38 #[must_use]
39 pub fn new<P: AsRef<OsStr>>(
40 name: &str,
41 #[cfg(feature = "complete")] cli: clap::Command,
42 outdir: P,
43 ) -> Self {
44 Self {
45 name: name.to_string(),
46 #[cfg(feature = "complete")]
47 cli,
48 outdir: Path::new(&outdir).to_path_buf(),
49 }
50 }
51
52 #[cfg(feature = "complete")]
53 fn gencomp(&self, gen: &str, outdir: &Path) -> Result<(), Box<dyn Error>> {
54 let mut cmd = self.cli.clone();
55 let path = match gen {
56 "bash" => generate_to(shells::Bash, &mut cmd, &self.name, outdir)?,
57 "fish" => generate_to(shells::Fish, &mut cmd, &self.name, outdir)?,
58 "nu" => generate_to(Nushell, &mut cmd, &self.name, outdir)?,
59 "pwsh" => generate_to(shells::PowerShell, &mut cmd, &self.name, outdir)?,
60 "zsh" => generate_to(shells::Zsh, &mut cmd, &self.name, outdir)?,
61 "elvish" => generate_to(shells::Elvish, &mut cmd, &self.name, outdir)?,
62 _ => unimplemented!(),
63 };
64 println!(" {}", path.display());
65 Ok(())
66 }
67
68 #[cfg(feature = "complete")]
69 pub fn completions(&self) -> Result<(), Box<dyn Error>> {
92 println!("Generating completions:");
93 ["bash", "fish", "nu", "pwsh", "zsh", "elvish"]
94 .iter()
95 .try_for_each(|gen| {
96 let mut outdir = self.outdir.clone();
97 let base = match *gen {
98 "bash" => ["share", "bash-completion", "completions"],
99 "zsh" => ["share", "zsh", "site-functions"],
100 "nu" => ["share", "nu", "completions"],
101 "pwsh" => ["share", "pwsh", "completions"],
102 "fish" => ["share", "fish", "completions"],
103 "elvish" => ["share", "elvish", "completions"],
104 _ => unimplemented!(),
105 };
106 base.iter().for_each(|d| outdir.push(d));
107 let mut dirbuilder = fs::DirBuilder::new();
108 dirbuilder.recursive(true).mode(0o0755);
109 if !outdir.exists() {
110 dirbuilder.create(&outdir)?;
111 }
112 self.gencomp(gen, &outdir)
113 })?;
114 Ok(())
115 }
116
117 fn copy_bin(&self, target_dir: Option<String>) -> Result<(), Box<dyn Error>> {
118 println!("Copying binary:");
119 let mut bindir = self.outdir.clone();
120 bindir.push("bin");
121 let mut dirbuilder = fs::DirBuilder::new();
122 dirbuilder.recursive(true).mode(0o0755);
123 if !bindir.exists() {
124 dirbuilder.create(&bindir)?;
125 }
126 let mut outfile = bindir;
127 outfile.push(&self.name);
128 let infile: PathBuf = if let Some(target_dir) = target_dir {
129 [&target_dir, &self.name].iter().collect()
130 } else {
131 ["target", "release", &self.name].iter().collect()
132 };
133 if !infile.exists() {
134 eprintln!("Error: you must run \"cargo build --release\" first");
135 }
136 fs::copy(&infile, &outfile)?;
137 println!(" {} -> {}", infile.display(), outfile.display());
138 Ok(())
139 }
140
141 fn compile_translation(&self, potfile: &str, lang: &str) -> Result<(), io::Error> {
142 let infile: PathBuf = ["po", potfile].iter().collect();
143 let mut lcdir = self.outdir.clone();
144 ["share", "locale", lang, "LC_MESSAGES"]
145 .iter()
146 .for_each(|d| lcdir.push(d));
147 if !lcdir.exists() {
148 fs::create_dir_all(&lcdir)?;
149 }
150 let mut outfile = lcdir.clone();
151 outfile.push(&self.name);
152 outfile.set_extension("mo");
153 let output = process::Command::new("msgfmt")
154 .args([
155 infile.to_str().ok_or(io::Error::other("Bad path"))?,
156 "-o",
157 outfile.to_str().ok_or(io::Error::other("Bad path"))?,
158 ])
159 .output()?;
160 if !output.status.success() {
161 process::exit(output.status.code().unwrap_or(1));
162 }
163 println!(" {} -> {}", infile.display(), outfile.display());
164 Ok(())
165 }
166
167 pub fn translations<P: AsRef<Path>>(&self, podir: P) -> Result<(), Box<dyn Error>> {
171 fs::read_dir(podir)?.try_for_each(|e| {
172 match e {
173 Err(e) => return Err(e),
174 Ok(entry) => {
175 if entry
176 .path()
177 .extension()
178 .ok_or(io::Error::other("Bad extension"))?
179 == "po"
180 {
181 let Some(lang) = entry
182 .file_name()
183 .to_str()
184 .ok_or(io::Error::other("PathError"))?
185 .strip_suffix(".po")
186 .map(ToString::to_string)
187 else {
188 return Err(io::Error::other("File path error"));
189 };
190 let path = entry
191 .path()
192 .to_str()
193 .ok_or(io::Error::other("Path error"))
194 .map(ToString::to_string)?;
195 self.compile_translation(&path, &lang)?;
196 }
197 }
198 }
199 Ok(())
200 })?;
201 Ok(())
202 }
203
204 #[cfg(feature = "mangen")]
205 pub fn manpage(&self, section: u8) -> Result<(), io::Error> {
227 let fname = format!("{}.{section}", &self.name);
228 println!("Generating manpage {fname}:");
229 let command = self.cli.clone();
230 let mut outdir = self.outdir.clone();
231 ["share", "man", &format!("man{section}")]
232 .iter()
233 .for_each(|d| outdir.push(d));
234 let mut dirbuilder = fs::DirBuilder::new();
235 dirbuilder.recursive(true).mode(0o0755);
236 if !outdir.exists() {
237 dirbuilder.create(&outdir)?;
238 }
239 let mut outfile = outdir;
240 outfile.push(fname);
241 let man = Man::new(command);
242 let mut buffer: Vec<u8> = vec![];
243 man.render(&mut buffer)?;
244 fs::write(&outfile, buffer)?;
245 println!(" {}", outfile.display());
246 Ok(())
247 }
248
249 #[cfg(feature = "icons")]
250 fn png(&self, tree: &Tree, size: u32, source: &Path) -> Result<(), Box<dyn Error>> {
251 let transform = Transform::from_scale(1.0, 1.0);
252 let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new(size, size) else {
253 return Err(String::from("Error creating png").into());
254 };
255 let tree = resvg::Tree::from_usvg(tree);
256 tree.render(transform, &mut pixmap.as_mut());
257 let mut outdir = self.outdir.clone();
258 let sizedir = format!("{size}x{size}");
259 ["share", "icons", "hicolor", &sizedir, "apps"]
260 .iter()
261 .for_each(|d| outdir.push(d));
262 let mut dirbuilder = fs::DirBuilder::new();
263 dirbuilder.recursive(true).mode(0o0755);
264 if !outdir.exists() {
265 dirbuilder.create(&outdir)?;
266 }
267 let mut outfile = outdir;
268 let fname = source
269 .file_stem()
270 .and_then(|x| x.to_str().map(|x| format!("{x}.png")))
271 .unwrap();
272 outfile.push(&fname);
273 println!(" {} -> {}", source.display(), outfile.display());
274 pixmap.save_png(outfile)?;
275 Ok(())
276 }
277
278 #[cfg(feature = "icons")]
279 pub fn icons<P: AsRef<OsStr>>(&self, source: Option<P>) -> Result<(), Box<dyn Error>> {
304 println!("Creating png icons from svg:");
305 let infile = if let Some(s) = source {
306 Path::new(&s).to_path_buf()
307 } else {
308 let mut p = ["data", &self.name].iter().collect::<PathBuf>();
309 p.set_extension("svg");
310 p
311 };
312 eprintln!("infile: {}", infile.display());
313 let data = fs::read(&infile)?;
314 let tree = Tree::from_data(&data, &Options::default())?;
315 for size in [256, 128, 64, 48, 32, 24, 16] {
316 self.png(&tree, size, &infile)?;
317 }
318 Ok(())
319 }
320
321 pub fn docfiles<P: AsRef<Path> + AsRef<OsStr>>(
335 &self,
336 files: &[P],
337 doc_subdir: &Path,
338 ) -> Result<(), Box<dyn Error>> {
339 println!("Copying documentation");
340 let docdir: PathBuf = [&self.outdir, &"share".into(), &"doc".into(), doc_subdir]
341 .iter()
342 .collect();
343 if !docdir.exists() {
344 fs::create_dir_all(&docdir)?;
345 }
346 files.iter().try_for_each(|f| {
347 let infile = PathBuf::from(f);
348 let Some(filename) = infile.file_name().map(PathBuf::from) else {
349 return Err(io::Error::other("Bad path").into());
350 };
351 let outfile: PathBuf = [&docdir, &filename].iter().collect();
352 fs::copy(&infile, &outfile)?;
353 println!(" {} -> {}", infile.display(), outfile.display());
354 Ok::<(), Box<dyn Error>>(())
355 })
356 }
357
358 pub fn install(
361 &self,
362 target_dir: Option<String>,
363 #[cfg(feature = "mangen")] section: u8,
364 ) -> Result<(), Box<dyn Error>> {
365 self.copy_bin(target_dir)?;
366 #[cfg(feature = "complete")]
367 self.completions()?;
368 #[cfg(feature = "mangen")]
369 self.manpage(section)?;
370 #[cfg(feature = "icons")]
371 {
372 let infile: Option<&str> = None;
373 self.icons(infile)?;
374 }
375 Ok(())
376 }
377}