diff options
-rw-r--r-- | pkgs/profpatsch/default.nix | 5 | ||||
-rw-r--r-- | pkgs/profpatsch/nman/default.nix | 18 | ||||
-rw-r--r-- | pkgs/profpatsch/nman/nman.go | 137 | ||||
-rw-r--r-- | pkgs/profpatsch/nman/nman.rs | 283 |
4 files changed, 293 insertions, 150 deletions
diff --git a/pkgs/profpatsch/default.nix b/pkgs/profpatsch/default.nix index 063b3acc..0fd31aeb 100644 --- a/pkgs/profpatsch/default.nix +++ b/pkgs/profpatsch/default.nix @@ -127,7 +127,9 @@ in rec { display-infos = callPackage ./display-infos { inherit sfttime; }; git-commit-index = callPackage ./git-commit-index { inherit script; }; nix-http-serve = callPackage ./nix-http-serve {}; - nman = callPackage ./nman {}; + nman = callPackage ./nman { + inherit writeRustSimpleBin; + }; sfttime = callPackage ./sfttime {}; show-qr-code = callPackage ./show-qr-code {}; warpspeed = callPackage ./warpspeed { @@ -164,6 +166,7 @@ in rec { inherit (import ./write-rust.nix { inherit pkgs runExeclineLocal getBins drvSeqL; }) writeRustSimple + writeRustSimpleBin writeRustSimpleLib testRustSimple ; 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)); + }, + } +} |