about summary refs log tree commit diff
path: root/pkgs/profpatsch/nman/nman.rs
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/profpatsch/nman/nman.rs')
-rw-r--r--pkgs/profpatsch/nman/nman.rs283
1 files changed, 283 insertions, 0 deletions
diff --git a/pkgs/profpatsch/nman/nman.rs b/pkgs/profpatsch/nman/nman.rs
new file mode 100644
index 00000000..150d9cbe
--- /dev/null
+++ b/pkgs/profpatsch/nman/nman.rs
@@ -0,0 +1,283 @@
+use std::cmp::Ordering;
+use std::ffi::{OsStr,OsString};
+use std::io::{Error, ErrorKind, self, Write};
+use std::os::unix::ffi::{OsStrExt, OsStringExt};
+use std::path::{Path, PathBuf};
+use std::process::{Command,ExitStatus};
+
+struct TempDir {
+    inner: Vec<u8>,
+}
+
+impl AsRef<Path> for TempDir {
+    fn as_ref(&self) -> &Path {
+        OsStr::from_bytes(&self.inner[..]).as_ref()
+    }
+}
+
+impl Drop for TempDir {
+    fn drop(&mut self) {
+        std::fs::remove_dir_all(self.as_ref());
+        std::fs::remove_dir(self.as_ref());
+    }
+}
+
+fn mktemp(suffix: &str) -> std::io::Result<TempDir> {
+    let mut mktemp = Command::new("mktemp")
+                    .arg("-d")
+                    .arg("--suffix")
+                    .arg(suffix)
+                    .output()?;
+
+    if mktemp.status.success() {
+        // remove trailing newline
+        mktemp.stdout.pop();
+        Ok(TempDir {
+            inner: mktemp.stdout
+        })
+    } else {
+        Err(Error::new(ErrorKind::Other, "mktemp exited with a non-zero status"))
+    }
+}
+
+enum NmanError {
+    NoTempDir,
+    Instantiate,
+    Build,
+    Man,
+    NotFound,
+    NixParseError,
+    Usage,
+}
+
+// TODO(sterni): return 127 for cmd not found
+fn nman_error_exit_code(err: NmanError) -> i32 {
+    match err {
+        NmanError::NoTempDir => 9,
+        NmanError::Instantiate => 10,
+        NmanError::Build => 11,
+        NmanError::Man => 12,
+        NmanError::NotFound => 1,
+        NmanError::NixParseError => 69, // EX_SOFTWARE
+        NmanError::Usage => 64, // EX_USAGE
+    }
+}
+
+#[derive(PartialEq, PartialOrd, Eq, Ord)]
+enum DrvOutput<'a> {
+    Man,
+    DevMan,
+    Doc,
+    DevDoc,
+    Out,
+    Info,
+    Dev,
+    Bin,
+    Lib,
+    Other(&'a [u8]),
+}
+
+struct DrvWithOutput<'a> {
+    path: &'a [u8],
+    output: DrvOutput<'a>,
+    rendered: &'a [u8],
+}
+
+fn parse_output<'a>(output: &'a [u8]) -> DrvOutput<'a> {
+    match output {
+        b"out" => DrvOutput::Out,
+        b"bin" => DrvOutput::Bin,
+        b"lib" => DrvOutput::Lib,
+        b"man" => DrvOutput::Man,
+        b"dev" => DrvOutput::Dev,
+        b"devdoc" => DrvOutput::DevDoc,
+        b"devman" => DrvOutput::DevMan,
+        _ => DrvOutput::Other(output),
+    }
+}
+
+fn parse_drv_path<'a>(drv_path: &'a [u8]) -> Option<DrvWithOutput<'a>> {
+    let mut split = drv_path.split(|c| char::from(*c) == '!');
+    let path = split.next().filter(|s| s.len() > 0)?;
+    let output = split.next()
+                      .map(parse_output)
+                      .unwrap_or(DrvOutput::Out);
+
+    match split.next() {
+        None => Some(DrvWithOutput {
+            path: path,
+            output: output,
+            rendered: drv_path,
+        }),
+        Some(_) => None,
+    }
+}
+
+fn build_man_page(drv: DrvWithOutput, section: &str, page: &str, tempdir: &TempDir) -> Result<Option<PathBuf>, NmanError> {
+    let mut build = Command::new("nix-store")
+                            .arg("--realise")
+                            .arg(OsStr::from_bytes(drv.rendered))
+                            .arg("--add-root")
+                            .arg(tempdir.as_ref().join("build-result"))
+                            .arg("--indirect")
+                            .output()
+                            .map_err(|_| NmanError::Build)?;
+
+    if !build.status.success() {
+        return Err(NmanError::Build);
+    }
+
+    // trailing newline
+    build.stdout.pop();
+
+    // TODO(sterni): 😑😑😑😑😑😑😑😑😑😑😑
+    let mut path = PathBuf::from(OsString::from_vec(build.stdout))
+                       .join("share/man")
+                       .join(format!("man{}", section))
+                       .join(page);
+
+    path.set_extension(format!("{}.gz", section));
+
+    if path.exists() {
+        Ok(Some(path))
+    } else {
+        // check for uncompressed man page if the derivation
+        // has no / a custom fixupPhase
+        path.set_extension(section);
+
+        if path.exists() {
+            Ok(Some(path))
+        } else {
+            Ok(None)
+        }
+    }
+}
+
+fn open_man_page(attr: &str, section: &str, page: &str) -> Result<(), NmanError> {
+    let tmpdir = mktemp("-nman").map_err(|_| NmanError::NoTempDir)?;
+    let expr = format!("with (import <nixpkgs> {{}}); builtins.map (o: {}.\"${{o}}\") {}.outputs", attr, attr);
+    let inst = Command::new("nix-instantiate")
+                       .arg("-E")
+                       .arg(expr)
+                       .arg("--add-root")
+                       .arg(tmpdir.as_ref().join("instantiation-result"))
+                       .arg("--indirect")
+                       .output()
+                       .map_err(|_| NmanError::Instantiate)?;
+
+    if !inst.status.success() {
+        return Err(NmanError::Instantiate);
+    }
+
+    let mut drvs: Vec<DrvWithOutput> =
+            inst.stdout.split(|c| char::from(*c) == '\n')
+                .filter_map(parse_drv_path).collect();
+
+    // the sort order is such that the outputs where we
+    // expect the man page to be are checked first.
+    // This means we realise the least amount of outputs
+    // necessary
+    //
+    // TODO(sterni): change sorting depending on section:
+    //               "3" and "3p" should prioritize DevMan
+    drvs.sort_unstable_by(|a, b| a.output.cmp(&b.output));
+
+    if drvs.len() <= 0 {
+        return Err(NmanError::NixParseError);
+    }
+
+    for drv in drvs {
+        let man_file = build_man_page(drv, section, page, &tmpdir)?;
+
+        match man_file {
+            None => continue,
+            Some(f) => {
+                let res = Command::new("man")
+                                  .arg("-l").arg(f)
+                                  .spawn()
+                                  .and_then(|mut c| c.wait())
+                                  .map(|c| c.success());
+
+                return match res {
+                    Ok(true) => Ok(()),
+                    _ => Err(NmanError::Man),
+                };
+            },
+        }
+    }
+
+    Err(NmanError::NotFound)
+}
+
+fn parse_man_section(section: &str) -> Result<&str, &str> {
+    match section {
+        "3p" => Ok(section),
+        _ => match u8::from_str_radix(section, 10) {
+            Ok(_) => Ok(section),
+            Err(_)  => Err("Invalid man section: not a number and not \"3p\""),
+        },
+    }
+}
+
+enum CliAction<'a> {
+    Usage,
+    // attribute, section, page
+    // TODO(sterni): section should be an option type, so we can implement
+    //               the search logic as man(1) does. Also worth considering
+    //               would be to not find the man page ourselves, but to just
+    //               set MANPATH and let man(1) do the searching. Use case
+    //               would be to allow
+    //                  nman libunwind unw_apply_reg_state
+    //              to work like
+    //                  nman libunwind 3 unw_apply_reg_state
+    Man(&'a str, &'a str, &'a str),
+}
+
+fn main() -> std::io::Result<()> {
+    fn dispatch_action(progname: &str, action: CliAction) -> std::io::Result<()> {
+        match action {
+            CliAction::Usage => {
+                println!("Usage: {} ATTR [SECTION | PAGE] [PAGE]", progname);
+                Ok(())
+            },
+            CliAction::Man(attr, section, page) =>
+                match open_man_page(attr, section, page) {
+                    Ok(_) => Ok(()),
+                    // TODO(sterni): print error message
+                    Err(t) => std::process::exit(nman_error_exit_code(t)),
+                },
+        }
+    }
+
+    let (opts, args) : (Vec<String>, Vec<String>) =
+            std::env::args().partition(|s| s.starts_with("-"));
+
+    let mut action : Result<CliAction, &str> = match args.len() {
+        2 => Ok(CliAction::Man(&args[1], "1", &args[1])),
+        3 => Ok(match parse_man_section(&args[2]) {
+            Ok(sec) => CliAction::Man(&args[1], sec, &args[1]),
+            Err(_) => CliAction::Man(&args[1], "1", &args[2]),
+        }),
+        4 => parse_man_section(&args[2])
+                .map(|s| CliAction::Man(&args[1], s, &args[3])),
+        _ => Err("Unexpected number of arguments"),
+    };
+
+    for opt in opts {
+        match &opt[..] {
+            "--help" | "--usage" | "-h" =>
+                action = Ok(CliAction::Usage),
+            _ => action = Err("Unknown option"),
+        }
+    }
+
+    match action {
+        Ok(action) => dispatch_action(&args[0], action),
+        Err(msg) => {
+            // TODO(sterni): stderr
+            println!("usage error: {}", msg);
+            dispatch_action(&args[0], CliAction::Usage);
+            std::process::exit(nman_error_exit_code(NmanError::Usage));
+        },
+    }
+}