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}