From 12274118ae49a720f1f777d1b38339b8dd734753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benno=20F=C3=BCnfst=C3=BCck?= Date: Sun, 31 Aug 2014 16:59:47 +0200 Subject: [PATCH] initial commit --- README.md | 36 +++++++++++++++++++ default.nix | 17 +++++++++ nix-bang.hs | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 README.md create mode 100644 default.nix create mode 100755 nix-bang.hs diff --git a/README.md b/README.md new file mode 100644 index 0000000..43c1a33 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +nix-bang +======== + +The Nix-bang script allows you to define dependencies at the top of scripts which will then be used to create a nix-shell environment when run. This project is only useful if you're using the nix package manager. + +Installation +------------ + +Clone the repository and run: + +``` +$ nix-env -if. +``` + +This will install the `nix-bang` and `nix-bangi` files into your user profile. + +Usage +----- + +To use `nix-bang`, you need to add a header to your file. Here is an example for a haskell file: + +```haskell +#!nix-bang +#!> haskell +#! haskell | text lens optparse-applicative +#! shell | nix nix-prefetch-scripts +``` + +The first line just tells the shell to use `nix-bang` when executing the script. The next line is then read by `nix-bang` to determine the language used for running the script. In this case, we tell `nix-bang` that we want haskell, so it will use `runhaskell` to execute the script. + +The next lines the specify dependencies of the script. The first entry on each line is the language of the following dependencies. This is required so that language-specific names can be converted to the correct nix attribute names. You should have one line per language. In our case, we say that we want to use the `text`, `lens` and `optparse-applicative` haskell packages. We also want that `nix` and `nix-prefetch-scripts` are available in $PATH (the `shell` language doesn't apply any renaming to their dependencies and just passes them through unmodified). + +Contributing +------------ + +If you want to add support for another language, or just have a good improvment you'd like to implement, feel free to fork the repository and then submit a pull request. You can find me on irc.freenode.org as bennofs in the #nixos channel if you have questions. diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..22d9540 --- /dev/null +++ b/default.nix @@ -0,0 +1,17 @@ +{ pkgs ? import {} }: + +pkgs.stdenv.mkDerivation { + name = "nix-bang"; + src = ./nix-bang.hs; + phases = [ "buildPhase" "installPhase" "fixupPhase" ]; + buildPhase = ''mkdir $out; ghc -O2 $src -o $out/nix-bang -odir $TMP''; + installPhase = ''ln -s $out/nix-bang $out/nix-bangi''; + buildInputs = [ (pkgs.haskellPackages.ghcWithPackages (hs: with hs; [lens text])) ]; + meta = { + homepage = https://github.com/bennofs/nix-bang; + description = "A shebang for running inside nix-shell."; + license = pkgs.lib.licenses.bsd3; + maintainers = [ pkgs.lib.maintainers.bennofs ]; + platforms = pkgs.haskellPackages.ghc.meta.platforms; + }; +} \ No newline at end of file diff --git a/nix-bang.hs b/nix-bang.hs new file mode 100755 index 0000000..8b8e4f9 --- /dev/null +++ b/nix-bang.hs @@ -0,0 +1,101 @@ +#!nix-bang +#!> haskell +#! haskell | text lens +#! shell | nix +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TupleSections #-} + +import Control.Monad +import Control.Applicative +import System.Environment +import Data.List +import Data.Text (Text) +import Data.Text.Lens (_Text) +import Control.Lens +import Control.Exception.Lens +import System.IO.Error.Lens +import System.Exit +import System.Posix.Process +import System.Posix.IO +import System.IO +import Data.Char +import Data.Monoid + +import qualified Data.Text as Text + +-- | Information about a languages +data LangDef = LangDef + { name :: String -- ^ Name of this language + , deps :: [Text] -> [Text] -- ^ Convert langunage-specific dependencies to nix packages + , run :: FilePath -> (String, [String]) -- ^ Command to run the given file as script + , repl :: FilePath -> (String, [String]) -- ^ Command to load the given file in an interpreter + } + +languages :: [LangDef] +languages = [haskell, shell] + +haskell :: LangDef +haskell = LangDef "haskell" d r i where + d pkgs = return $ + "haskellPackages.ghcWithPackages (hs: with hs; [" <> Text.unwords (map mkPkg pkgs) <> "])" + mkPkg = Text.concat . over (_tail.mapped._head) toUpper . Text.splitOn "-" + r script = ("runhaskell" , [script]) + i script = ("ghci" , [script]) + +shell :: LangDef +shell = LangDef "shell" (extraPackages ++) r i where + r script = ("bash", [script]) + i _ = ("bash", []) + extraPackages = ["bash", "coreutils", "utillinux", "gitAndTools.hub", "git"] + +lookupLangDef :: String -> IO LangDef +lookupLangDef n + | Just def <- find ((n ==) . name) languages = return def + | otherwise = fail $ "Unknown language: " ++ n + +makeDeps :: String -> [String] -> IO [String] +makeDeps lang ds = lookupLangDef lang <&> \def -> + map (view _Text) $ deps def (map (review _Text) ds) + +parseDepLine :: [String] -> IO (String, [String]) +parseDepLine (lang:"|":deps) = return (lang, deps) +parseDepLine x = fail $ "Invalid dependency specification: " ++ unwords x + +makeCommand :: String -> Bool -> String -> IO (String, [String]) +makeCommand lang interactive file = lookupLangDef lang <&> \def -> + (if interactive then repl else run) def file + +makeEnvArg :: String -> IO String +makeEnvArg env = f $ getEnv env <&> \val -> env ++ "=" ++ val where + f = handling_ (_IOException.errorType._NoSuchThing) $ return "" + +makeXargsCommand :: String -> Int -> IO String +makeXargsCommand cmd fd = do + let xargsFile = "/proc/self/fd/" ++ show fd + envStr <- unwords <$> traverse makeEnvArg + ["LOCALE_ARCHIVE", "LANG", "TERMINFO", "TERM"] + return $ "env " ++ envStr ++ " xargs -a " ++ xargsFile ++ " -d '\\n' " ++ cmd ++ "" + +main :: IO () +main = do + progName <- getProgName + args <- getArgs + let interactive = "i" `isSuffixOf` progName + case args ^? _Cons of + Nothing -> fail $ "usage: " ++ progName ++ " " ++ " [missing file name]" + Just (file, args') -> do + header <- drop 1 . map (drop 2) . takeWhile ("#!" `isPrefixOf`) . lines <$> readFile file + case header ^? _Cons of + Just ('>':lang, depHeader) -> do + deps <- concat <$> traverse (uncurry makeDeps <=< parseDepLine . words) depHeader + let deps' = "findutils" : deps + let depArgs = concatMap (\x -> ["-p", x]) deps' + (cmd,cmdArgs) <- makeCommand (under _Text Text.strip lang) interactive file + (readFd, writeFd) <- createPipe + writeH <- fdToHandle writeFd + hPutStrLn writeH (unlines cmdArgs) >> hFlush writeH + hClose writeH + xargsCmd <- makeXargsCommand cmd (fromIntegral readFd) + let finalArgs = "--pure" : "--command" : xargsCmd : depArgs + executeFile "nix-shell" True finalArgs Nothing + _ -> fail "missing language to run as"