Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nix integration #4149

Merged
merged 3 commits into from
Dec 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Cabal.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{ mkDerivation, array, base, binary, bytestring, containers
, deepseq, directory, exceptions, filepath, old-time, pretty
, process, QuickCheck, regex-posix, stdenv, tagged, tasty
, tasty-hunit, tasty-quickcheck, time, transformers, unix
}:
mkDerivation {
pname = "Cabal";
version = "1.25.0.0";
src = ./Cabal;
libraryHaskellDepends = [
array base binary bytestring containers deepseq directory filepath
pretty process time unix
];
testHaskellDepends = [
array base bytestring containers directory exceptions filepath
old-time pretty process QuickCheck regex-posix tagged tasty
tasty-hunit tasty-quickcheck time transformers unix
];
doCheck = false;
homepage = "http://www.haskell.org/cabal/";
description = "A framework for packaging Haskell software";
license = stdenv.lib.licenses.bsd3;
}
1 change: 1 addition & 0 deletions Cabal/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Welcome to the Cabal User Guide
concepts-and-development
bugs-and-stability
nix-local-build-overview
nix-integration
40 changes: 40 additions & 0 deletions Cabal/doc/nix-integration.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
Nix Integration
===============

`Nix <http://nixos.org/nix/>`_ is a package manager popular with some Haskell developers due to its focus on reliability and reproducibility. ``cabal`` now has the ability to integrate with Nix for dependency management during local package development.

Enabling Nix Integration
------------------------

To enable Nix integration, simply pass the ``--enable-nix`` global option when you call ``cabal``. To use this option everywhere, edit your ``$HOME/.cabal/config`` file to include:

.. code-block:: cabal

nix: True

If the package (which must be locally unpacked) provides a ``shell.nix`` file, this flag will cause ``cabal`` to run most commands through ``nix-shell``. The following commands are affected:

- ``cabal configure``
- ``cabal build``
- ``cabal repl``
- ``cabal install`` (only if installing into a sandbox)
- ``cabal haddock``
- ``cabal freeze``
- ``cabal gen-bounds``
- ``cabal run``

If the package does not provide a ``shell.nix``, ``cabal`` runs normally.

Creating Nix Expressions
------------------------

The Nix package manager is based on a lazy, pure, functional programming language; packages are defined by expressions in this language. The fastest way to create a Nix expression for a Cabal package is with the `cabal2nix <https://github.com/NixOS/cabal2nix>`_ tool. To create a ``shell.nix`` expression for the package in the current directory, run this command:

.. code-block:: console

$ cabal2nix --shell ./. >shell.nix

Further Reading
----------------

The `Nix manual <http://nixos.org/nix/manual/#chap-writing-nix-expressions>`_ provides further instructions for writing Nix expressions. The `Nixpkgs manual <http://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure>`_ describes the infrastructure provided for Haskell packages.
35 changes: 35 additions & 0 deletions cabal-install.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{ mkDerivation, array, async, base, base16-bytestring, binary
, bytestring, Cabal, containers, cryptohash-sha256, deepseq
, directory, filepath, hackage-security, hashable, HTTP, mtl
, network, network-uri, pretty, pretty-show, process, QuickCheck
, random, regex-posix, stdenv, stm, tagged, tar, tasty, tasty-hunit
, tasty-quickcheck, time, unix, zlib
}:
mkDerivation {
pname = "cabal-install";
version = "1.25.0.0";
src = ./cabal-install;
isLibrary = false;
isExecutable = true;
setupHaskellDepends = [ base Cabal filepath process ];
executableHaskellDepends = [
array async base base16-bytestring binary bytestring Cabal
containers cryptohash-sha256 deepseq directory filepath
hackage-security hashable HTTP mtl network network-uri pretty
process random stm tar time unix zlib
];
testHaskellDepends = [
array async base base16-bytestring binary bytestring Cabal
containers cryptohash-sha256 deepseq directory filepath
hackage-security hashable HTTP mtl network network-uri pretty
pretty-show process QuickCheck random regex-posix stm tagged tar
tasty tasty-hunit tasty-quickcheck time unix zlib
];
postInstall = ''
mkdir $out/etc
mv bash-completion $out/etc/bash_completion.d
'';
homepage = "http://www.haskell.org/cabal/";
description = "The command-line interface for Cabal and Hackage";
license = stdenv.lib.licenses.bsd3;
}
3 changes: 2 additions & 1 deletion cabal-install/Distribution/Client/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ instance Semigroup SavedConfig where
globalRequireSandbox = combine globalRequireSandbox,
globalIgnoreSandbox = combine globalIgnoreSandbox,
globalIgnoreExpiry = combine globalIgnoreExpiry,
globalHttpTransport = combine globalHttpTransport
globalHttpTransport = combine globalHttpTransport,
globalNix = combine globalNix
}
where
combine = combine' savedGlobalFlags
Expand Down
6 changes: 4 additions & 2 deletions cabal-install/Distribution/Client/GlobalFlags.hs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ data GlobalFlags = GlobalFlags {
globalRequireSandbox :: Flag Bool,
globalIgnoreSandbox :: Flag Bool,
globalIgnoreExpiry :: Flag Bool, -- ^ Ignore security expiry dates
globalHttpTransport :: Flag String
globalHttpTransport :: Flag String,
globalNix :: Flag Bool -- ^ Integrate with Nix
} deriving Generic

defaultGlobalFlags :: GlobalFlags
Expand All @@ -85,7 +86,8 @@ defaultGlobalFlags = GlobalFlags {
globalRequireSandbox = Flag False,
globalIgnoreSandbox = Flag False,
globalIgnoreExpiry = Flag False,
globalHttpTransport = mempty
globalHttpTransport = mempty,
globalNix = Flag False
}

instance Monoid GlobalFlags where
Expand Down
179 changes: 179 additions & 0 deletions cabal-install/Distribution/Client/Nix.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ViewPatterns #-}

module Distribution.Client.Nix
( findNixExpr
, inNixShell
, nixInstantiate
, nixShell
, nixShellIfSandboxed
) where

#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
#endif

import Control.Exception (catch)
import Control.Monad (filterM, when, unless)
import System.Directory
( createDirectoryIfMissing, doesDirectoryExist, doesFileExist
, makeAbsolute, removeDirectoryRecursive, removeFile )
import System.Environment (getExecutablePath, getArgs, lookupEnv)
import System.FilePath
( (</>), (<.>), replaceExtension, takeDirectory, takeFileName )
import System.IO (IOMode(..), hClose, openFile)
import System.IO.Error (isDoesNotExistError)
import System.Process (showCommandForUser)

import Distribution.Compat.Semigroup

import Distribution.Verbosity

import Distribution.Simple.Program
( Program(..), ProgramDb
, addKnownProgram, configureProgram, emptyProgramDb, getDbProgramOutput
, runDbProgram, simpleProgram )
import Distribution.Simple.Setup (fromFlagOrDefault)
import Distribution.Simple.Utils (debug, existsAndIsMoreRecentThan)

import Distribution.Client.Config (SavedConfig(..))
import Distribution.Client.GlobalFlags (GlobalFlags(..))
import Distribution.Client.Sandbox.Types (UseSandbox(..))


configureOneProgram :: Verbosity -> Program -> IO ProgramDb
configureOneProgram verb prog =
configureProgram verb prog (addKnownProgram prog emptyProgramDb)


touchFile :: FilePath -> IO ()
touchFile path = do
catch (removeFile path) (\e -> when (isDoesNotExistError e) (return ()))
createDirectoryIfMissing True (takeDirectory path)
openFile path WriteMode >>= hClose


findNixExpr :: GlobalFlags -> SavedConfig -> IO (Maybe FilePath)
findNixExpr globalFlags config = do
-- criteria for deciding to run nix-shell
let nixEnabled =
fromFlagOrDefault False
(globalNix (savedGlobalFlags config) <> globalNix globalFlags)

if nixEnabled
then do
let exprPaths = [ "shell.nix", "default.nix" ]
filterM doesFileExist exprPaths >>= \case
[] -> return Nothing
(path : _) -> return (Just path)
else return Nothing


nixInstantiate
:: Verbosity
-> FilePath
-> Bool
-> GlobalFlags
-> SavedConfig
-> IO ()
nixInstantiate verb dist force globalFlags config =
findNixExpr globalFlags config >>= \case
Nothing -> return ()
Just shellNix -> do
alreadyInShell <- inNixShell
shellDrv <- drvPath dist shellNix
instantiated <- doesFileExist shellDrv
-- an extra timestamp file is necessary because the derivation lives in
-- the store so its mtime is always 1.
let timestamp = shellDrv <.> "timestamp"
upToDate <- existsAndIsMoreRecentThan timestamp shellNix

let ready = alreadyInShell || (instantiated && upToDate && not force)
unless ready $ do

let prog = simpleProgram "nix-instantiate"
progdb <- configureOneProgram verb prog

removeGCRoots verb dist
touchFile timestamp

_ <- getDbProgramOutput verb prog progdb
[ "--add-root", shellDrv, "--indirect", shellNix ]
return ()


nixShell
:: Verbosity
-> FilePath
-> GlobalFlags
-> SavedConfig
-> IO ()
-- ^ The action to perform inside a nix-shell. This is also the action
-- that will be performed immediately if Nix is disabled.
-> IO ()
nixShell verb dist globalFlags config go = do

alreadyInShell <- inNixShell

if alreadyInShell
then go
else do
findNixExpr globalFlags config >>= \case
Nothing -> go
Just shellNix -> do

let prog = simpleProgram "nix-shell"
progdb <- configureOneProgram verb prog

cabal <- getExecutablePath

-- Run cabal with the same arguments inside nix-shell.
-- When the child process reaches the top of nixShell, it will
-- detect that it is running inside the shell and fall back
-- automatically.
shellDrv <- drvPath dist shellNix
args <- getArgs
runDbProgram verb prog progdb
[ "--add-root", gcrootPath dist </> "result", "--indirect", shellDrv
, "--run", showCommandForUser cabal args
]


drvPath :: FilePath -> FilePath -> IO FilePath
drvPath dist path =
-- Nix garbage collector roots must be absolute paths
makeAbsolute (dist </> "nix" </> replaceExtension (takeFileName path) "drv")


gcrootPath :: FilePath -> FilePath
gcrootPath dist = dist </> "nix" </> "gcroots"


inNixShell :: IO Bool
inNixShell = maybe False (const True) <$> lookupEnv "IN_NIX_SHELL"


removeGCRoots :: Verbosity -> FilePath -> IO ()
removeGCRoots verb dist = do
let tgt = gcrootPath dist
exists <- doesDirectoryExist tgt
when exists $ do
debug verb ("removing Nix gcroots from " ++ tgt)
removeDirectoryRecursive tgt


nixShellIfSandboxed
:: Verbosity
-> FilePath
-> GlobalFlags
-> SavedConfig
-> UseSandbox
-> IO ()
-- ^ The action to perform inside a nix-shell. This is also the action
-- that will be performed immediately if Nix is disabled.
-> IO ()
nixShellIfSandboxed verb dist globalFlags config useSandbox go =
case useSandbox of
NoSandbox -> go
UseSandbox _ -> nixShell verb dist globalFlags config go
3 changes: 2 additions & 1 deletion cabal-install/Distribution/Client/ProjectConfig/Legacy.hs
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,8 @@ convertToLegacySharedConfig
globalRequireSandbox = mempty,
globalIgnoreSandbox = mempty,
globalIgnoreExpiry = projectConfigIgnoreExpiry,
globalHttpTransport = projectConfigHttpTransport
globalHttpTransport = projectConfigHttpTransport,
globalNix = mempty
}

configFlags = mempty {
Expand Down
53 changes: 38 additions & 15 deletions cabal-install/Distribution/Client/Reconfigure.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Distribution.Client.Reconfigure ( Check(..), reconfigure ) where

import Control.Monad ( unless, when )
import Data.Maybe ( isJust )
import Data.Monoid hiding ( (<>) )
import System.Directory ( doesFileExist )

Expand All @@ -15,6 +16,7 @@ import Distribution.Simple.Utils

import Distribution.Client.Config ( SavedConfig(..) )
import Distribution.Client.Configure ( readConfigFlags )
import Distribution.Client.Nix ( findNixExpr, inNixShell, nixInstantiate )
import Distribution.Client.Sandbox
( WereDepsReinstalled(..), findSavedDistPref, getSandboxConfigFilePath
, maybeReinstallAddSourceDeps, updateInstallDirs )
Expand Down Expand Up @@ -111,21 +113,42 @@ reconfigure

savedFlags@(_, _) <- readConfigFlags dist

let checks =
checkVerb
<> checkDist
<> checkOutdated
<> check
<> checkAddSourceDeps
(Any force, flags@(configFlags, _)) <- runCheck checks mempty savedFlags

let (_, config') =
updateInstallDirs
(configUserInstall configFlags)
(useSandbox, config)

when force $ configureAction flags extraArgs globalFlags
return config'
useNix <- fmap isJust (findNixExpr globalFlags config)
alreadyInNixShell <- inNixShell

if useNix && not alreadyInNixShell
then do

-- If we are using Nix, we must reinstantiate the derivation outside
-- the shell. Eventually, the caller will invoke 'nixShell' which will
-- rerun cabal inside the shell. That will bring us back to 'reconfigure',
-- but inside the shell we'll take the second branch, below.

-- This seems to have a problem: won't 'configureAction' call 'nixShell'
-- yet again, spawning an infinite tree of subprocesses?
-- No, because 'nixShell' doesn't spawn a new process if it is already
-- running in a Nix shell.

nixInstantiate verbosity dist False globalFlags config
return config

else do

let checks =
checkVerb
<> checkDist
<> checkOutdated
<> check
<> checkAddSourceDeps
(Any force, flags@(configFlags, _)) <- runCheck checks mempty savedFlags

let (_, config') =
updateInstallDirs
(configUserInstall configFlags)
(useSandbox, config)

when force $ configureAction flags extraArgs globalFlags
return config'

where

Expand Down
Loading