about summary refs log tree commit diff
path: root/pkgs/profpatsch/nman
diff options
context:
space:
mode:
authorsternenseemann <0rpkxez4ksa01gb3typccl0i@systemli.org>2021-02-12 14:05:56 +0100
committersternenseemann <sternenseemann@systemli.org>2021-02-25 19:52:33 +0100
commitcbef421081f1e65b1dcd81c9a6434e69cbc50f8e (patch)
tree64d8bb5209903dc803e5c12c328524c1f7cf64a0 /pkgs/profpatsch/nman
parent9f11b3333709364c15a60c38fee9b655728b6c93 (diff)
pkgs/profpatsch/nman: initial rewrite in rust
This is a complete and user interface compatible rewrite of nman.go in
Rust which aleviates a few flaws of the previous implementation:

* Garbage collector roots for both the derivation and the build outputs
  are created in a temporary directory which is cleaned afterwards. This
  prevents a race condition between nman and nix-store --gc and doesn't
  pollute the user's working directory.
* Instead of building just one build output, nman now checks every
  output for the requested man page which fixes nman for derivations
  which have their man pages split between multiple outputs, e. g. man
  and devman.

Future work:

* Improve error messages reported to the user
* Man page
* Implement man page search heuristics if no section is specified:
  Instead of falling back to section 1, all sections should be checked
  for the desired page (in an ascending order?)
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));
+        },
+    }
+}