sequoia_man/lib.rs
1/// Generate Unix manual pages for sq from its `clap::Command` value.
2///
3/// A Unix manual page is a document marked up with the
4/// [troff](https://en.wikipedia.org/wiki/Troff) language. The troff
5/// markup is the source code for the page, and is formatted and
6/// displayed using the "man" command.
7///
8/// Troff is a child of the 1970s and is one of the earlier markup
9/// languages. It has little resemblance to markup languages born in
10/// the 21st century, such as Markdown. However, it's not actually
11/// difficult, merely old, and sometimes weird. Some of the design of
12/// the troff language was dictated by the constraints of 1970s
13/// hardware, programming languages, and fashions in programming. Let
14/// not those scare you.
15///
16/// The troff language supports "macros", a way to define new commands
17/// based on built-in commands. There are a number of popular macro
18/// packages for various purposes. One of the most popular ones for
19/// manual pages is called "man", and this module generates manual
20/// pages for that package. It's supported by the "man" command on all
21/// Unix systems.
22///
23/// Note that this module doesn't aim to be a generic manual page
24/// generator. The scope is specifically the Sequoia sq command.
25
26use std::env;
27use std::fs;
28use std::io::Write;
29use std::path::Path;
30use std::path::PathBuf;
31
32use anyhow::Context;
33
34pub mod man;
35
36type Result<T, E=anyhow::Error> = std::result::Result<T, E>;
37
38/// Variable name to control the asset out directory with.
39pub const ASSET_OUT_DIR: &str = "ASSET_OUT_DIR";
40
41/// Returns the directory to write the given assets to.
42///
43/// For man pages, this would usually be `man-pages`.
44///
45/// The base directory is takens from the environment variable
46/// [`ASSET_OUT_DIR`] or, if that is not set, cargo's [`OUT_DIR`]:
47///
48/// > OUT_DIR — If the package has a build script, this is set to the
49/// > folder where the build script should place its output. See below
50/// > for more information. (Only set during compilation.)
51///
52/// [`OUT_DIR`]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
53///
54/// This function panics if neither environment variable is set.
55pub fn asset_out_dir(asset: &str) -> Result<PathBuf> {
56    println!("cargo:rerun-if-env-changed={}", ASSET_OUT_DIR);
57    let outdir: PathBuf =
58        env::var_os(ASSET_OUT_DIR).unwrap_or_else(
59            || env::var_os("OUT_DIR").expect("OUT_DIR not set")).into();
60    if outdir.exists() && ! outdir.is_dir() {
61        return Err(anyhow::anyhow!("{}={:?} is not a directory",
62                                   ASSET_OUT_DIR, outdir));
63    }
64
65    let path = outdir.join(asset);
66    fs::create_dir_all(&path)?;
67    Ok(path)
68}
69
70/// pandoc helper file to convert a man page to HTML.
71pub const MAN_PANDOC_LUA: &[u8] = include_bytes!("man-pandoc.lua");
72
73/// pandoc helper file to convert a man page to HTML.
74pub const MAN_PANDOC_INC_HTML: &[u8] = include_bytes!("man-pandoc.inc.html");
75
76/// Generates man pages.
77///
78/// `asset_dir` is the directory where the man pages will be written.
79///
80/// `version` is the bare version string, which is usually obtained
81/// from `env!("CARGO_PKG_VERSION")`.
82///
83/// If `extra_version` is `Some`, then the version is created `version
84/// (extra_version)`.
85///
86/// The helper files `man-pandoc.lua`, `man-pandoc.inc.html` and
87/// `man2html.sh`, will also be written to the directory.
88///
89/// If you define a data type `Cli`, then you would do:
90///
91/// ```no_run
92/// use clap::CommandFactory;
93/// use clap::Parser;
94/// #
95///
96/// #[derive(Parser, Debug)]
97/// #[clap(
98///    name = "sq",
99///    about = "A command-line frontend for Sequoia, an implementation of OpenPGP")]
100/// struct Cli {
101///     // ...
102/// }
103///
104/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
105/// let dir = sequoia_man::asset_out_dir("man-pages")?;
106/// let mut cli = Cli::command();
107/// let mut builder = sequoia_man::man::Builder::new(&mut cli, env!("CARGO_PKG_VERSION"), None);
108/// builder.see_also(&[ "For the full documentation see <https://...>." ]);
109/// sequoia_man::generate_man_pages(&dir, &builder)?;
110/// # Ok(())
111/// # }
112/// ```
113///
114/// To convert the man pages to HTML, run the `man2html.sh` script.
115///
116/// ```shell
117/// bash .../man-pages/man2html.sh
118/// ```
119pub fn generate_man_pages(asset_dir: &Path, builder: &man::Builder)
120    -> Result<()>
121{
122    let mut man2html = String::new();
123
124    man2html.push_str("#! /bin/bash\n");
125    man2html.push_str("# Convert the man pages to HTML using pandoc.\n");
126    man2html.push_str("\n");
127    man2html.push_str("set -e\n\n");
128    man2html.push_str("cd $(dirname $0)\n\n");
129
130    man2html.push_str("FILES=\"");
131    for man in builder.build() {
132        man2html.push_str(&format!(" {}", man.filename().display()));
133        std::fs::write(asset_dir.join(man.filename()), man.troff_source())?;
134    }
135    man2html.push_str("\"\n");
136    man2html.push_str("\n");
137
138    man2html.push_str(&format!("\
139case \"$1\" in
140  --generate)
141    for man_page in $FILES
142    do
143      BINARY={} pandoc -s $man_page -L man-pandoc.lua -H man-pandoc.inc.html -o $man_page.html
144    done
145    ;;
146  --man-files)
147    for man_page in $FILES
148    do
149      echo $man_page
150    done
151    ;;
152  --man-root)
153    for man_page in $FILES
154    do
155      echo $man_page
156      break
157    done
158    ;;
159  --html-files)
160    for man_page in $FILES
161    do
162      echo $man_page.html
163    done
164    ;;
165  --html-root)
166    for man_page in $FILES
167    do
168      echo $man_page.html
169      break
170    done
171    ;;
172  *)
173    echo \"Usage: $0 --generate|--man-files|--man-root|--html-files|--html-root\"
174    exit 1
175    ;;
176esac
177", builder.binary()));
178
179    let target = asset_dir.join("man-pandoc.lua");
180    std::fs::write(&target, MAN_PANDOC_LUA)
181        .with_context(|| format!("Writing {}", target.display()))?;
182
183    let target = asset_dir.join("man-pandoc.inc.html");
184    std::fs::write(&target, MAN_PANDOC_INC_HTML)
185        .with_context(|| format!("Writing {}", target.display()))?;
186
187    let target = asset_dir.join("man2html.sh");
188    let mut f = std::fs::File::create(&target)
189        .with_context(|| format!("Crating {}", target.display()))?;
190    f.write_all(man2html.as_bytes())?;
191
192    #[cfg(unix)]
193    {
194        use std::os::unix::fs::PermissionsExt;
195
196        // Make it executable for the owner.
197        let metadata = f.metadata()?;
198        let mut permissions = metadata.permissions();
199        permissions.set_mode(permissions.mode() | 0o100);
200        f.set_permissions(permissions)?;
201    }
202
203    println!("cargo:warning=man pages written to {}", asset_dir.display());
204
205    Ok(())
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    use clap::CommandFactory;
213    use clap::Parser;
214
215    #[derive(Parser, Debug)]
216    #[clap(
217        name = "frob",
218        about = "A tool to help with frobnication.")]
219    struct Cli {
220        /// How intense to frobnicate.
221        #[arg(long="intensity")]
222        intensity: usize,
223    }
224
225    #[test]
226    fn build() {
227        let dir = tempfile::TempDir::new().unwrap();
228        let mut cli = Cli::command();
229        let mut builder = man::Builder::new(
230            &mut cli, env!("CARGO_PKG_VERSION"), None);
231        builder.see_also(&[ "For the full documentation see <https://...>." ]);
232        generate_man_pages(dir.path(), &builder).unwrap();
233
234        // Persist the state:
235        if false {
236            let p = dir.into_path();
237            eprintln!("Persisted output to: {}", p.display());
238        }
239    }
240}