about summary refs log tree commit diff
path: root/pkgs/profpatsch/nman
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/profpatsch/nman')
-rw-r--r--pkgs/profpatsch/nman/default.nix18
-rw-r--r--pkgs/profpatsch/nman/nman.go137
-rw-r--r--pkgs/profpatsch/nman/nman.rs283
3 files changed, 289 insertions, 149 deletions
diff --git a/pkgs/profpatsch/nman/default.nix b/pkgs/profpatsch/nman/default.nix
index 96699833..9d74d63b 100644
--- a/pkgs/profpatsch/nman/default.nix
+++ b/pkgs/profpatsch/nman/default.nix
@@ -1,14 +1,8 @@
-{ lib, runCommand, go }:
+{ lib, writeRustSimpleBin }:
 
-runCommand "nman" {
-  meta = with lib; {
-    description = "Invoke manpage in temporary nix-shell";
-    license = licenses.gpl3;
+writeRustSimpleBin "nman" {
+  meta = {
+    license = lib.licenses.gpl3Only;
+    description = "Open man page in a temporary nix-shell";
   };
-} ''
-    mkdir cache
-    env GOCACHE="$PWD/cache" \
-      ${lib.getBin go}/bin/go build -o nman ${./nman.go}
-    install -D nman $out/bin/nman
-''
-
+} ./nman.rs
diff --git a/pkgs/profpatsch/nman/nman.go b/pkgs/profpatsch/nman/nman.go
deleted file mode 100644
index b62479a0..00000000
--- a/pkgs/profpatsch/nman/nman.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"os"
-	"strconv"
-	"log"
-	"bytes"
-	"os/exec"
-	"io/ioutil"
-	"syscall"
-)
-
-var usage = `nman drvAttr [section|page] [page]
-
-Open man pages in a temporary nix-shell.
-1 If one argument is given, the drvAttr & page have the same name.
-2 If two arguments are given and the second arg is
-    a <number>) like 1, but in the man section <number>
-    a <page>  ) like 1, but open the page <page>
-3 If three arguments are given, the order is <drvAttr> <sect> <page>
-`
-
-func lookupManPage(manpath string, manSection int64, manPage string) (int, error) {
-	if manSection < -1 {
-		panic("manSection must not be < -1")
-	}
-	// holy debug printf
-	// fmt.Printf("attr: %s, sect: %d, page: %s\n", drvAttr, manSection, manPage)
-
-	manBin, err := exec.LookPath("man");
-	if err != nil {
-		return 0, fmt.Errorf("man executable not in PATH")
-	}
-
-	var manArgs []string
-	if (manSection == -1) {
-		manArgs = []string{manPage}
-	} else {
-		manArgs = []string{strconv.FormatInt(manSection, 10), manPage}
-	}
-
-	man := exec.Command(manBin, manArgs...)
-
-        man.Env = append(
-		os.Environ(),
-		fmt.Sprintf("MANPATH=%s", manpath))
-	man.Stderr = os.Stderr
-	man.Stdout = os.Stdout
-	err = man.Run()
-	if exiterr, ok := err.(*exec.ExitError); ok {
-		ws := exiterr.Sys().(syscall.WaitStatus)
-		return ws.ExitStatus(), nil
-	} else {
-		return 0, err
-	}
-
-}
-
-func buildManOutput(drvAttr string) (string, error) {
-	nixExpr := fmt.Sprintf(
-		`with import <nixpkgs> {}; %s.man or %s.doc or %s.out or %s`,
-		drvAttr, drvAttr, drvAttr, drvAttr)
-
-	nixBuild := exec.Command("nix-build", "--no-out-link", "-E", nixExpr)
-	nixBuild.Stderr = os.Stderr
-	pipe, err := nixBuild.StdoutPipe()
-	if err != nil { return "", fmt.Errorf("could not access stdout of nix-build: %s", err) }
-
-	err = nixBuild.Start()
-	if err != nil { return "", fmt.Errorf("could not start nix-build: %s", err) }
-
-	out, err := ioutil.ReadAll(pipe)
-	if err != nil { return "", fmt.Errorf("could not read from nix-build: %s", err) }
-
-	lines := bytes.Split(bytes.TrimSpace(out), []byte("\n"))
-	err = nixBuild.Wait()
-	if err != nil { return "", fmt.Errorf("nix-build process died: %s", err) }
-
-	// last line ouf output looks like: /nix/store/abc…-foobar.drv!output
-	drvPath := bytes.Split(lines[len(lines)-1], []byte("!"))[0]
-	if _, err := os.Stat(string(drvPath)); err != nil {
-		return "", fmt.Errorf("%s doesn’t look like an output path: %s", drvPath, err)
-	}
-
-	return string(drvPath), nil
-}
-
-func main() {
-	var manPage string
-	var drvAttr string
-
-	args := os.Args
-
-	// man section or -1 if no man section
-        var manSection int64 = -1
-	if (len(args) >= 3) {
-		i, err := strconv.ParseUint(args[2], 10, 64)
-		if err == nil && i >= 0 {
-			manSection = int64(i)
-		}
-	}
-
-	// the first argument is always a derivation attribute
-	switch len(args) {
-	case 2:
-		// arg is package and drv attr
-		manPage = args[1]
-	case 3:
-		if (manSection == -1) {
-			// like 2, but arg 2 is package
-			manPage = args[2]
-		} else {
-			// man section given, page is arg 1
-			manPage = args[1]
-		}
-	case 4:
-		// arg 2 is manSection, arg 3 is package
-		manPage = args[3]
-	default:
-		fmt.Print(usage)
-		os.Exit(-1)
-	}
-
-	drvAttr = args[1]
-	drvPath, err := buildManOutput(drvAttr)
-	if err != nil {
-		log.Fatal(err)
-	}
-	exitStatus, err := lookupManPage(drvPath + "/share/man", manSection, manPage)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	os.Exit(exitStatus)
-
-}
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));
+        },
+    }
+}