{-# LANGUAGE CPP #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE OverloadedStrings #-} module Main where import System.Process import Control.Monad.Identity #if MIN_VERSION_optparse_applicative(0,13,0) import Data.Monoid ((<>)) #endif import Data.Yaml hiding (Parser) import Data.Yaml.Config import qualified Data.Map as M import Data.Map (Map) import Data.Aeson.Types (typeMismatch) import Options.Applicative import Data.List (intercalate) import System.Directory (createDirectoryIfMissing, canonicalizePath) import Prelude hiding (log) parens :: [Char] -> [Char] parens x = "(" ++ x ++ ")" ----------------------------------------- -- OPTIONS data Command = Configure | Cabal [String] | Clean | Command :<> Command withInfo :: Parser a -> String -> ParserInfo a withInfo opts desc = info (helper <*> opts) $ progDesc desc parseExec :: Parser Command parseExec = (\rest -> Cabal (["exec","--"] ++ rest)) <$> some (argument str (metavar "COMMAND")) parseRepl :: Parser Command parseRepl = (\target -> Cabal (["repl"] ++ maybe [] (:[]) target)) <$> optional (argument str (metavar "TARGET")) parseCabal :: Parser Command parseCabal = Cabal <$> some (argument str (metavar "COMMAND")) parseCommand :: Parser Command parseCommand = subparser $ command "configure" (pure Configure `withInfo` "Re-configure the project on the basis of the styx.yaml file") <> command "clean" (pure Clean `withInfo` "Remove all styx working files") <> command "build" (pure (Cabal ["install","--only-dependencies", "--force-reinstalls"] :<> Cabal ["install","--avoid-reinstalls"]) `withInfo` "(Attempt to) build and install all the packages in the sandbox") <> command "repl" (parseRepl `withInfo` "Start a repl in the nix-shell'ed cabal sandbox") <> command "exec" (parseExec `withInfo` "Exec a command in the nix-shell'ed cabal sandbox") <> command "cabal" (parseCabal `withInfo` "Execute an arbitrary cabal command in the nix-shell'ed cabal sandbox") main :: IO () main = run =<< execParser (parseCommand `withInfo` "Wrapper around nix-shell, cabal2nix and cabal") -------------------------------- -- Configuration data Repo = Repo {repoLocation :: String, repoRevision :: Maybe String, repoCabal2NixFlags :: Maybe String} data Config = Config {cfgNixpkgsVersion :: Maybe SourceVersion ,cfgLocalPackages :: Map String Repo -- list of local packages (must be on the local filesystem) ,cfgExternalSourceDeps :: Map String Repo -- mapping of package names to locations as understood by cabal2nix ,cfgNixHsDeps :: [String] -- ^ haskell deps to fetch directly from nix (usually empty for a cabal project, as the cabal file will specifiy deps) ,cfgNixOtherDeps :: [String] -- ^ Other nix dependencies (non haskell packages) ,cfgDefCompil :: String } data ShellConfig = ShellConfig {} instance FromJSON Config where parseJSON (Object v) = Config <$> v .:? "nixpkgs" <*> v .:? "local-packages" .!= M.empty <*> v .:? "source-deps" .!= M.empty <*> v .:? "nix-deps" .!= [] <*> v .:? "non-haskell-deps" .!= [] <*> v .:? "default-compiler" .!= "ghc801" parseJSON invalid = typeMismatch "Config" invalid instance FromJSON Repo where parseJSON (Object v) = Repo <$> v .: "location" <*> -- location of the repo (in cabal2nix format) v .:? "revision" <*> v .:? "cabal2nix" parseJSON invalid = typeMismatch "Location" invalid data SourceVersion = GitVersion {gitOwner :: String, gitCommit :: String, gitSha :: String} | TarballVersion {tarballURL :: String} instance FromJSON SourceVersion where parseJSON (Object v) = (GitVersion <$> v .:? "owner" .!= "NixOS" <*> v .: "commit" <*> v .: "sha256") <|> (TarballVersion <$> v .: "url") parseJSON invalid = typeMismatch "Git version" invalid ----------------------------------------- -- Program locToNix :: String -> Repo -> IO () locToNix p (Repo {..}) = do cmd $ intercalate " " ["cabal2nix", maybe "" ("--revision=" ++) repoRevision, maybe "" id repoCabal2NixFlags, repoLocation, "> .styx/" ++ p ++ ".nix"] canonicalizeLocalPath :: Repo -> IO Repo canonicalizeLocalPath (Repo {repoLocation = d,..}) = do repoLocation <- canonicalizePath d return (Repo {..}) run :: Command -> IO () run c = case c of a :<> b -> run a >> run b Configure -> configure Cabal args -> do _ <- cmd ("nix-shell .styx/shell.nix --pure --run " ++ show (intercalate " " ("cabal":args))) return () Clean -> cmd "rm -rf .styx" cmd :: String -> IO () cmd x = do putStrLn x callCommand x log :: String -> IO () log msg = putStrLn $ "Styx: " ++ msg configure :: IO () configure = do Config{..} <- loadYamlSettings ["styx.yaml"] [] ignoreEnv createDirectoryIfMissing False ".styx" log "Running cabal2nix for all local and external packages" forM_ (M.assocs cfgLocalPackages) $ \(p,r) -> locToNix p =<< (canonicalizeLocalPath r) forM_ (M.assocs cfgExternalSourceDeps) (uncurry locToNix) log "Initializing a sandbox in .styx" cmd "cabal sandbox init --sandbox .styx" log "Adding local packages as sources to the sandbox" forM_ (M.assocs cfgLocalPackages) $ \(_,Repo {..}) -> do cmd ("cabal sandbox add-source " ++ repoLocation) log "Creating shell.nix file" writeFile ".styx/shell.nix" $ unlines $ ["{ nixpkgs ? import {}, compiler ? " ++ (show cfgDefCompil) ++ " }:"] ++ case cfgNixpkgsVersion of Nothing -> ["let nixpkgs' = nixpkgs;"] Just source -> ["let nixpkgs_source ="] ++ case source of GitVersion {..} -> [" nixpkgs.fetchFromGitHub {" ," owner = " ++ show gitOwner ++ ";" ," repo = \"nixpkgs\";" ," rev = " ++ show gitCommit ++ ";" ," sha256 = " ++ show gitSha ++ ";" ," };" ] TarballVersion {..} -> ["url = " ++ show tarballURL ++ ";"] ++ [" nixpkgs' = (import nixpkgs_source){};"] ++ ["in with nixpkgs'.pkgs;" ,"let hp = haskell.packages.${compiler}.override{" ," overrides = self: super: {" ] ++ [" " ++ n ++ " = self.callPackage ./" ++ n ++ ".nix {};" | n <- (M.keys cfgExternalSourceDeps ++ M.keys cfgLocalPackages)] ++ [" };};" ," getHaskellDeps = ps: path:" ," let f = import path;" ," gatherDeps = { " ++ concat [d ++ " ? [], " | d <- depKinds] ++ "...}:" ," " ++ intercalate " ++ " depKinds ++ ";" ," x = f (builtins.intersectAttrs (builtins.functionArgs f) ps // {stdenv = stdenv; mkDerivation = gatherDeps;});" ," in x;" ,"ghc = hp.ghcWithPackages (ps: with ps; stdenv.lib.lists.subtractLists" , "[" ++ intercalate " " (M.keys cfgLocalPackages) ++ "]" -- Here we remove the packages that we provide locally in the sandbox , "([ cabal-install " , intercalate " " (M.keys cfgExternalSourceDeps ++ cfgNixHsDeps) ," ] " ++ concat [" ++ getHaskellDeps ps ./" ++ n ++ ".nix"| n <- M.keys cfgLocalPackages] ++ "));" ,"in" ,"pkgs.stdenv.mkDerivation {" ," name = \"my-haskell-env-0\";" ," buildInputs = [ ghc " ++ intercalate " " (map parens cfgNixOtherDeps) ++ "];" -- todo system build inputs here ," shellHook = ''" ," export LANG=en_US.UTF-8" ," eval $(egrep ^export ${ghc}/bin/ghc)" ,"'';" ,"}"] -- run (Cabal ["configure"]) -- this will fail unless the sandbox dependencies are built first. depKinds :: [String] depKinds = ["buildDepends", "libraryHaskellDepends", "executableHaskellDepends", "libraryToolDepends", "executableToolDepends"]