package_bootstrap/
lib.rs

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
29/// a command installer
30pub 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    /// Generates and installs shell completions for all supported shells into
70    /// `dir`.
71    /// ## Examples
72    /// To install shell completions under /usr:
73    /// ```no_run
74    /// use package_bootstrap::Bootstrap;
75    ///
76    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
77    ///     Bootstrap::new("foo", clap::Command::new("foo"), "/usr")
78    ///         .completions()?;
79    ///     Ok(())
80    /// }
81    /// ```
82    /// To install into a staging directory for packaging purposes:
83    /// ```no_run
84    /// use package_bootstrap::Bootstrap;
85    ///
86    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
87    ///     Bootstrap::new("foo", clap::Command::new("foo"), "staging/usr")
88    ///         .completions()?;
89    ///     Ok(())
90    /// }
91    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    /// Compiles and installs translations into <prefix>/share/locale using the
168    /// [msgfmt](https://www.gnu.org/software/gettext/manual/html_node/msgfmt-Invocation.html)
169    /// external utility.
170    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    /// Install a Unix man page base on the given `clap::Command` struct.
206    /// ## Example
207    /// Install a manual page for this command into /usr, in the man1 section:
208    /// ```no_run
209    /// use package_bootstrap::Bootstrap;
210    ///
211    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
212    ///     Bootstrap::new("foo", clap::Command::new("foo"), "/usr")
213    ///         .manpage(1)?;
214    ///     Ok(())
215    /// }
216    /// ```
217    /// Install a manual page into a staging directory in the man8 section:
218    /// ```no_run
219    /// use package_bootstrap::Bootstrap;
220    ///
221    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
222    ///     Bootstrap::new("foo", clap::Command::new("foo"), "pkg/usr")
223    ///         .manpage(8)?;
224    ///     Ok(())
225    /// }
226    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    /// Generate sized png icons from a master svg icon
280    /// ## Example
281    /// Generate and install icons into /usr/share/icons
282    /// ```no_run
283    /// use package_bootstrap::Bootstrap;
284    /// use std::error::Error;
285    ///
286    /// fn main() -> Result<(), Box<dyn Error>> {
287    ///     Bootstrap::new("foo", clap::Command::new("foo"), "/usr")
288    ///         .icons(Some("data/foo.svg"))?;
289    ///     Ok(())
290    /// }
291    /// ```
292    /// Generate and install icons into a staging directory
293    /// ```no_run
294    /// use package_bootstrap::Bootstrap;
295    /// use std::error::Error;
296    ///
297    /// fn main() -> Result<(), Box<dyn Error>> {
298    ///     Bootstrap::new("foo", clap::Command::new("foo"), "pkg/usr")
299    ///         .icons(Some("data/foo.svg"))?;
300    ///     Ok(())
301    /// }
302    /// ```
303    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    /// Copies a slice of files to a documentation subdirectory.
322    /// # Example
323    /// Copy this crate's README and LICENSE files into <PREFIX>/share/doc/foo
324    /// ```no_run
325    /// use package_bootstrap::Bootstrap;
326    /// use std::error::Error;
327    /// use std::path::Path;
328    ///
329    /// fn main() -> Result<(), Box<dyn Error>> {
330    ///     Bootstrap::new("foo", clap::Command::new("foo"), "pkg/usr")
331    ///         .docfiles(&["README.md", "LICENSE.md"], &Path::new("foo"))?;
332    ///     Ok(())
333    /// }
334    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    /// Installs this crate's binary into <Prefix>/bin. If the "mangen" feature
359    /// is enabled, also generates and installs the program's manpage.
360    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}