├── release.nix ├── Setup.hs ├── .gitignore ├── src ├── Stack2nix │ ├── External │ │ ├── VCS.hs │ │ ├── Util.hs │ │ ├── VCS │ │ │ └── Git.hs │ │ ├── Cabal2nix.hs │ │ └── Stack.hs │ ├── External.hs │ ├── Types.hs │ ├── PP.hs │ ├── Hackage.hs │ ├── Util.hs │ └── Render.hs └── Stack2nix.hs ├── RELEASE.md ├── .travis.yml ├── nixpkgs-src.json ├── stack.yaml ├── README.md ├── scripts ├── check-nix-is-updated.sh ├── init-env.sh └── travis.sh ├── fetch-nixpkgs.nix ├── tests └── test.hs ├── LICENSE ├── default.nix ├── ChangeLog.md ├── stack2nix.cabal ├── stack2nix └── Main.hs └── .stylish-haskell.yaml /release.nix: -------------------------------------------------------------------------------- 1 | { 2 | stack2nix = import ./. {}; 3 | } 4 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .stack-work/ 3 | .cabal-sandbox/ 4 | .s2n/ 5 | cabal.sandbox.config 6 | 7 | *~ 8 | result 9 | -------------------------------------------------------------------------------- /src/Stack2nix/External/VCS.hs: -------------------------------------------------------------------------------- 1 | module Stack2nix.External.VCS 2 | ( module Stack2nix.External.VCS.Git 3 | ) where 4 | 5 | import Stack2nix.External.VCS.Git 6 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | - bump version in cabal 2 | - update changelog 3 | - git commit -m "v0.X.X" 4 | - git tag v0.X.X 5 | - stack sdist . 6 | - git push --tags origin HEAD 7 | - stack upload . 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: nix 2 | sudo: required 3 | before_script: 4 | - sudo mount -o remount,exec,size=4G,mode=755 /run/user || true 5 | script: 6 | - ./scripts/travis.sh 7 | - ./scripts/check-nix-is-updated.sh 8 | -------------------------------------------------------------------------------- /nixpkgs-src.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "NixOS", 3 | "repo": "nixpkgs", 4 | "rev": "872502aa56bd4d574fcfe9cfef9066c9e8ee2894", 5 | "sha256": "07kbsnrmcrr0nnb91vbm6p3ixww9c5fgia0drx14y2hcc0292s8s" 6 | } 7 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-13.15 2 | 3 | packages: 4 | - '.' 5 | 6 | flags: 7 | stack: 8 | disable-git-info: true 9 | 10 | nix: 11 | packages: [zlib,gmp,git,pcre,openssl] 12 | 13 | ghc-options: 14 | stack2nix: -Werror 15 | -------------------------------------------------------------------------------- /src/Stack2nix/External.hs: -------------------------------------------------------------------------------- 1 | module Stack2nix.External 2 | ( module Stack2nix.External.Cabal2nix 3 | , module Stack2nix.External.VCS 4 | ) where 5 | 6 | import Stack2nix.External.Cabal2nix 7 | import Stack2nix.External.VCS 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stack2nix 2 | 3 | [![Build Status](https://travis-ci.org/input-output-hk/stack2nix.svg?branch=master)](https://travis-ci.org/input-output-hk/stack2nix) 4 | [![Hackage](https://img.shields.io/hackage/v/stack2nix.svg)](https://hackage.haskell.org/package/stack2nix) 5 | 6 | [Deprecated](https://github.com/input-output-hk/stack2nix/issues/171#issuecomment-808663618) in favor of https://github.com/input-output-hk/haskell.nix 7 | -------------------------------------------------------------------------------- /scripts/check-nix-is-updated.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | readlink=$(nix-instantiate --eval -E "/. + (import ).coreutils")/readlink 4 | scriptDir=$(dirname -- "$($readlink -f -- "${BASH_SOURCE[0]}")") 5 | source $scriptDir/init-env.sh 6 | 7 | set -xe 8 | 9 | fail_stack2nix_check() { 10 | echo "ERROR: you need to run './scripts/check-nix-is-updated.sh' and commit the changes" >&2 11 | exit 1 12 | } 13 | 14 | \time ~/.local/bin/stack2nix --hackage-snapshot 2019-01-16T08:56:04Z . > $scriptDir/../stack2nix.nix 15 | 16 | git diff --text --exit-code || fail_stack2nix_check 17 | -------------------------------------------------------------------------------- /scripts/init-env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | readlink=$(nix-instantiate --eval -E "/. + (import ).coreutils")/readlink 4 | export scriptDir=$(dirname -- "$($readlink -f -- "${BASH_SOURCE[0]}")") 5 | NIXPKGS=$(nix-build "${scriptDir}/../fetch-nixpkgs.nix" --no-out-link) 6 | export NIX_PATH="nixpkgs=$NIXPKGS" 7 | 8 | 9 | set -ex 10 | 11 | STACK=$(nix-build -A stack $NIXPKGS)/bin 12 | NIX_PREFETCH=$(nix-build -A nix-prefetch-git $NIXPKGS)/bin 13 | GIT=$(nix-build -A git $NIXPKGS)/bin 14 | CABAL_INSTALL=$(nix-build -A cabal-install $NIXPKGS)/bin 15 | export PATH="$STACK:$NIX_PREFETCH:$GIT:$CABAL_INSTALL:$HOME/.local/bin:$PATH" 16 | -------------------------------------------------------------------------------- /fetch-nixpkgs.nix: -------------------------------------------------------------------------------- 1 | let 2 | spec = builtins.fromJSON (builtins.readFile ./nixpkgs-src.json); 3 | src = import { 4 | url = "https://github.com/${spec.owner}/${spec.repo}/archive/${spec.rev}.tar.gz"; 5 | inherit (spec) sha256; 6 | }; 7 | nixcfg = import ; 8 | in builtins.derivation { 9 | system = builtins.currentSystem; 10 | name = "${src.name}-unpacked"; 11 | builder = builtins.storePath nixcfg.shell; 12 | inherit src; 13 | args = [ 14 | (builtins.toFile "builder" '' 15 | $coreutils/mkdir $out 16 | cd $out 17 | $gzip -d < $src | $tar -x --strip-components=1 18 | '') 19 | ]; 20 | coreutils = builtins.storePath nixcfg.coreutils; 21 | tar = builtins.storePath nixcfg.tar; 22 | gzip = builtins.storePath nixcfg.gzip; 23 | } 24 | -------------------------------------------------------------------------------- /tests/test.hs: -------------------------------------------------------------------------------- 1 | 2 | import Data.Version (makeVersion) 3 | import Stack2nix.Util 4 | import Test.Hspec 5 | 6 | 7 | main :: IO () 8 | main = hspec $ do 9 | describe "Stack2nix.Util" $ do 10 | it "cabal-install version extraction" $ do 11 | extractVersion "cabal-install version 2.0.0.0" `shouldBe` Just (makeVersion [2, 0, 0, 0]) 12 | it "git version extraction" $ do -- issue #67 13 | extractVersion "git version 2.11.0 (Apple Git-81)" `shouldBe` Just (makeVersion [2, 11, 0]) 14 | it "cabal2nix version extraction" $ do 15 | extractVersion "cabal2nix 2.7" `shouldBe` Just (makeVersion [2, 7]) 16 | it "ghc version extraction" $ do 17 | extractVersion "The Glorious Glasgow Haskell Compilation System, version 8.0.2" `shouldBe` Just (makeVersion [8, 0, 2]) 18 | -------------------------------------------------------------------------------- /src/Stack2nix/Types.hs: -------------------------------------------------------------------------------- 1 | module Stack2nix.Types where 2 | 3 | import Data.Time (UTCTime) 4 | import Distribution.PackageDescription (FlagName) 5 | import Distribution.System (Platform) 6 | 7 | data Args = Args 8 | { argRev :: Maybe String 9 | , argOutFile :: Maybe FilePath 10 | , argStackYaml :: FilePath 11 | , argThreads :: Int 12 | , argTest :: Bool 13 | , argBench :: Bool 14 | , argHaddock :: Bool 15 | , argHackageSnapshot :: Maybe UTCTime 16 | , argPlatform :: Platform 17 | , argUri :: String 18 | , argIndent :: Bool 19 | , argVerbose :: Bool 20 | , argCabal2nixArgs :: Maybe String 21 | , argEnsureExecutables :: Bool 22 | } 23 | deriving (Show) 24 | 25 | type Flags = [(FlagName, Bool)] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 IOHK 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to 8 | do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/Stack2nix/External/Util.hs: -------------------------------------------------------------------------------- 1 | module Stack2nix.External.Util where 2 | 3 | import Data.Monoid ((<>)) 4 | import System.Directory (getCurrentDirectory) 5 | import System.Exit (ExitCode (..)) 6 | import System.Process (CreateProcess (..), proc, 7 | readCreateProcessWithExitCode) 8 | 9 | runCmdFrom :: FilePath -> String -> [String] -> IO (ExitCode, String, String) 10 | runCmdFrom dir prog args = readCreateProcessWithExitCode (fromDir dir (proc prog args)) "" 11 | where 12 | fromDir :: FilePath -> CreateProcess -> CreateProcess 13 | fromDir d procDesc = procDesc { cwd = Just d } 14 | 15 | runCmd :: String -> [String] -> IO (ExitCode, String, String) 16 | runCmd prog args = getCurrentDirectory >>= (\d -> runCmdFrom d prog args) 17 | 18 | failHard :: (ExitCode, String, String) -> IO (ExitCode, String, String) 19 | failHard r@(ExitSuccess, _, _) = pure r 20 | failHard (ExitFailure code, _, err) = 21 | error $ unlines [ "Failed with exit code " <> show code <> "..." 22 | , show err 23 | ] 24 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import (import ./fetch-nixpkgs.nix) {} }: 2 | 3 | with pkgs.haskell.lib; 4 | 5 | ((import ./stack2nix.nix { inherit pkgs; }).override { 6 | overrides = self: super: { 7 | # TODO: separate out output 8 | stack2nix = justStaticExecutables (overrideCabal super.stack2nix (old: { 9 | src = builtins.path { 10 | name = "stack2nix"; 11 | path = ./.; 12 | # Filter hidden dirs (.), e.g. .git and .stack-work 13 | # TODO Remove once https://github.com/input-output-hk/stack2nix/issues/119 is done 14 | filter = path: type: 15 | !(pkgs.lib.hasPrefix "." (baseNameOf path)) 16 | && baseNameOf path != "stack.yaml"; 17 | }; 18 | })); 19 | 20 | # https://github.com/commercialhaskell/lts-haskell/issues/149 21 | stack = doJailbreak super.stack; 22 | 23 | # needed until we upgrade to 18.09 24 | yaml = disableCabalFlag super.yaml "system-libyaml"; 25 | 26 | # https://github.com/NixOS/cabal2nix/issues/146 27 | hinotify = if pkgs.stdenv.isDarwin then self.hfsevents else super.hinotify; 28 | }; 29 | }).stack2nix 30 | -------------------------------------------------------------------------------- /src/Stack2nix/External/VCS/Git.hs: -------------------------------------------------------------------------------- 1 | module Stack2nix.External.VCS.Git 2 | ( Command(..), ExternalCmd(..), InternalCmd(..) 3 | , git 4 | ) where 5 | 6 | import Stack2nix.External.Util (failHard, runCmd, runCmdFrom) 7 | import System.Exit (ExitCode (..)) 8 | 9 | data Command = OutsideRepo ExternalCmd 10 | | InsideRepo FilePath InternalCmd 11 | data ExternalCmd = Clone String FilePath 12 | data InternalCmd = Checkout CommitRef 13 | 14 | type CommitRef = String 15 | 16 | exe :: String 17 | exe = "git" 18 | 19 | -- Requires git binary in PATH 20 | git :: Command -> IO (ExitCode, String, String) 21 | git (OutsideRepo cmd) = runExternal cmd 22 | git (InsideRepo dir cmd) = runInternal dir cmd 23 | 24 | runExternal :: ExternalCmd -> IO (ExitCode, String, String) 25 | runExternal (Clone uri dir) = 26 | runCmd exe ["clone", "--recurse-submodules", uri, dir] >>= failHard 27 | 28 | runInternal :: FilePath -> InternalCmd -> IO (ExitCode, String, String) 29 | runInternal repoDir (Checkout ref) = do 30 | checkoutCmd <- runCmdFrom repoDir exe ["checkout", ref] 31 | _ <- failHard checkoutCmd 32 | submoduleCmd <- runCmdFrom repoDir exe ["submodule", "update", "--init", "--recursive"] 33 | failHard submoduleCmd 34 | -------------------------------------------------------------------------------- /scripts/travis.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | readlink=$(nix-instantiate --eval -E "/. + (import ).coreutils")/readlink 4 | scriptDir=$(dirname -- "$($readlink -f -- "${BASH_SOURCE[0]}")") 5 | source $scriptDir/init-env.sh 6 | 7 | # build and install 8 | \time stack --nix install --test --fast --ghc-options="+RTS -A128m -n2m -RTS" 9 | 10 | # SMOKE TESTS 11 | 12 | export NIX_PATH=nixpkgs=https://github.com/NixOS/nixpkgs/archive/ef802feaceba073e3e5178af1b5d65f27f5cc93b.tar.gz 13 | 14 | # basic remote 15 | \time stack2nix --verbose -o /tmp/haskell-dummy-project1.nix \ 16 | --revision 31aac4dcc7b87d5cb62dafe9b9402346fdf449a6 \ 17 | https://github.com/jmitchell/haskell-dummy-project1.git 18 | \time nix-build -A haskell-dummy-package1 /tmp/haskell-dummy-project1.nix 19 | 20 | # multi remote 21 | \time stack2nix --verbose -o /tmp/haskell-multi-package-demo1.nix \ 22 | --revision e3d9bd6d6066dab5222ce53fb7d234f28eafa2d5 \ 23 | https://github.com/jmitchell/haskell-multi-package-demo1.git 24 | \time nix-build -A haskell-multi-proj-demo1 /tmp/haskell-multi-package-demo1.nix 25 | 26 | # multi local and ../ relpath 27 | TMP_REPO="$(mktemp -d)" 28 | git clone https://github.com/jmitchell/haskell-multi-package-demo1.git "$TMP_REPO" 29 | cd "$TMP_REPO" 30 | git checkout e3d9bd6d6066dab5222ce53fb7d234f28eafa2d5 31 | cd src 32 | \time stack2nix --verbose -o hmpd.nix ../ 33 | \time nix-build -A haskell-multi-proj-demo1 hmpd.nix 34 | test $(grep "src = ../dep1" hmpd.nix | wc -l) -eq 1 35 | -------------------------------------------------------------------------------- /src/Stack2nix/PP.hs: -------------------------------------------------------------------------------- 1 | module Stack2nix.PP 2 | (ppIndented, ppSingletons) where 3 | 4 | import Text.PrettyPrint (Doc, Mode(..), TextDetails(..), fullRender, render) 5 | 6 | -- | Formats the derivation doc with indentation and lines wrapped at 100 chars. 7 | ppIndented :: Doc -> String 8 | ppIndented = render 9 | 10 | -- | Formats the derivation doc without indentation and with each list 11 | -- element and function argument on its own line. 12 | -- We go to this effort to "ugly-print" so that the resulting file is 13 | -- less susceptible to merge conflicts when checked into git. 14 | ppSingletons :: Doc -> String 15 | ppSingletons doc = fixSpace . snd $ fullRender LeftMode 80 1.5 printer (0, "") doc 16 | where 17 | printer :: TextDetails -> (Int, String) -> (Int, String) 18 | printer (Chr c) (n, s) = ppChar n s c 19 | printer (Str s1) s2 = fmap (s1 ++) s2 20 | printer (PStr s1) s2 = fmap (s1 ++) s2 21 | 22 | ppChar :: Int -> String -> Char -> (Int, String) 23 | ppChar n s c = (n', s') 24 | where 25 | s' = case c of 26 | ',' -> "\n," ++ s 27 | ' ' -> (if n /= 0 then '\n' else ' '):s 28 | '{' -> "{\n " ++ s 29 | '}' -> "\n}" ++ s 30 | _ -> c:s 31 | n' = case c of 32 | '[' -> n - 1 -- PrettyPrint works backwards 33 | ']' -> n + 1 34 | _ -> n 35 | 36 | -- remove single trailing spaces 37 | fixSpace :: String -> String 38 | fixSpace (' ':'\n':s) = '\n':fixSpace s 39 | fixSpace (c:s) = c:fixSpace s 40 | fixSpace [] = [] 41 | -------------------------------------------------------------------------------- /src/Stack2nix/Hackage.hs: -------------------------------------------------------------------------------- 1 | -- | This module is needed to replace P.parseDB which would otherwise filter out 2 | -- deprecated packages, which may end up being in an LTS. 3 | module Stack2nix.Hackage 4 | ( loadHackageDB 5 | ) where 6 | 7 | import Control.Exception (SomeException, mapException) 8 | import qualified Data.Map as Map 9 | import Data.Time.Clock (UTCTime) 10 | import Distribution.Hackage.DB.Path (hackageTarball) 11 | import qualified Distribution.Hackage.DB.Unparsed as U 12 | import qualified Distribution.Hackage.DB.Parsed as P 13 | import Distribution.Package (PackageName) 14 | import Distribution.Hackage.DB.Errors (HackageDBPackageName(..)) 15 | import qualified Distribution.Nixpkgs.Haskell.Hackage as H 16 | 17 | 18 | 19 | loadHackageDB :: Maybe FilePath 20 | -- ^ The path to the Hackage database. 21 | -> Maybe UTCTime 22 | -- ^ If we have hackage-snapshot time. 23 | -> IO H.HackageDB 24 | loadHackageDB optHackageDB optHackageSnapshot = do 25 | dbPath <- maybe hackageTarball return optHackageDB 26 | readTarball optHackageSnapshot dbPath 27 | 28 | 29 | readTarball :: Maybe UTCTime -> FilePath -> IO H.HackageDB 30 | readTarball ts p = do 31 | dbu <- U.readTarball ts p 32 | let dbp = parseDB dbu 33 | return (Map.mapWithKey (H.parsePackageData dbu) dbp) 34 | 35 | 36 | parseDB :: U.HackageDB -> P.HackageDB 37 | parseDB = Map.mapWithKey parsePackageData 38 | 39 | parsePackageData :: PackageName -> U.PackageData -> P.PackageData 40 | parsePackageData pn (U.PackageData _ vs') = 41 | mapException (\e -> HackageDBPackageName pn (e :: SomeException)) $ 42 | Map.mapWithKey (P.parseVersionData pn) $ vs' 43 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.2.3 (2019-04-29) 4 | 5 | Added: 6 | 7 | - Add flag `--no-ensure-executables` to stop `stack2nix` from ensuring 8 | necessary executables exist #164 9 | - Allow extra cabal2nix flags #156 10 | 11 | Bug Fixes: 12 | 13 | - When writing nix expressions to file (`-o` flag), force UTF-8 encoding #164 14 | 15 | ## v0.2.2 (2019-01-17) 16 | 17 | Bug fixes: 18 | 19 | - LTS-13 compatibility 20 | - Proper fix for bin-package-db #118 21 | - Add package flags #148 22 | - Upgrade stack to 1.9 #133 23 | 24 | ## v0.2.1 (2018-09-04) 25 | 26 | Bug fixes: 27 | 28 | - null bin-package-db for GHC 7.10 #118 29 | - Bump cabal2nix and remove upper bound #120 30 | - Parse mac operating system as osx or darwin #123 31 | - Don't use `src = ./.` #121 32 | - Pass --compiler to calls to cabal2nix #115 33 | 34 | ## v0.2 (2018-07-24) 35 | 36 | Major changes: 37 | 38 | - Use full stackage snapshot instead of relying on the build plan #83 39 | - Get rid of hnix and rely on Derivation type from cabal2nix 40 | - Use nix to provision executables if missing #83 41 | - Use GHC version that belongs to the LTS #84 42 | - ghc-options in stack.yaml are now passed to generated Nix exprs #96 43 | - Support --bench #97 44 | 45 | Other enhancements: 46 | 47 | - Support --platform to set targeting system generation #79 48 | - Use cabal2nix and stack as haskell libraries instead of relying on executable parsing #75 49 | - Add --verbose flag #78 50 | - Be able to pin down hackage snapshot #75 51 | - Optimize cabal2nix calls by reusing HackageDB #75 52 | - Rewrite tests in hspec to reduce dependencies #83 53 | - Make stack.yaml filename configurable #90 54 | - Add option to disable indentation #89 55 | - When cloning git, also checkout submodules #108 56 | 57 | Bug fixes: 58 | 59 | - Be able to override GHC core packages #51 60 | - Cleanup concurrency #33 61 | - Add --haddock #38 62 | - Add --test #35 63 | - Support Stack subdirs #10 64 | - Correct version parsing #67 65 | - Silence git stdout output not to leak into Nix #91 66 | 67 | ## v0.1.3.0 (2017-07-27) 68 | 69 | Bug fixes: 70 | 71 | - Apply only Nix overrides without version fixes #26 72 | 73 | ## v0.1.2.0 (2017-06-22) 74 | 75 | Bug fixes: 76 | 77 | - Minor stack2nix.cabal improvements 78 | 79 | ## v0.1.1.0 (2017-06-22) 80 | 81 | Initial public release. 82 | -------------------------------------------------------------------------------- /src/Stack2nix/External/Cabal2nix.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE RecordWildCards #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Stack2nix.External.Cabal2nix ( 5 | cabal2nix 6 | ) where 7 | 8 | import Cabal2nix (cabal2nixWithDB, parseArgs, optNixpkgsIdentifier, Options) 9 | import Control.Lens 10 | import Data.Bool (bool) 11 | import Data.Maybe (fromMaybe, maybeToList) 12 | import Data.Text (Text, unpack) 13 | import qualified Distribution.Nixpkgs.Haskell.Hackage as DB 14 | import Distribution.Nixpkgs.Haskell.Derivation (Derivation) 15 | import Distribution.PackageDescription (unFlagName) 16 | import Distribution.System (Platform(..), Arch(..), OS(..)) 17 | import Language.Nix 18 | import System.IO (hPutStrLn, stderr) 19 | import Stack.Types.Version (Version) 20 | import Stack2nix.Types (Args (..), Flags) 21 | 22 | import Text.PrettyPrint.HughesPJClass (Doc) 23 | 24 | cabal2nix :: Args -> Version -> FilePath -> Maybe Text -> Maybe FilePath -> Flags -> DB.HackageDB -> IO (Either Doc Derivation) 25 | cabal2nix Args{..} ghcVersion uri commit subpath flags hackageDB = do 26 | let runCmdArgs = args $ fromMaybe "." subpath 27 | hPutStrLn stderr $ unwords ("+ cabal2nix":runCmdArgs) 28 | options <- parseArgs runCmdArgs 29 | cabal2nixWithDB hackageDB $ cabalOptions options 30 | 31 | where 32 | args :: FilePath -> [String] 33 | args dir = concat 34 | [ maybe [] (\c -> ["--revision", unpack c]) commit 35 | , maybeToList argCabal2nixArgs 36 | , ["--subpath", dir] 37 | , ["--system", fromCabalPlatform argPlatform] 38 | , ["--compiler", "ghc-" ++ show ghcVersion] 39 | , ["-f" ++ bool "-" "" enable ++ unFlagName f | (f, enable) <- flags] 40 | , [uri] 41 | ] 42 | 43 | -- Override default nixpkgs resolver to do pkgs.attr instead of attr 44 | cabalOptions :: Options -> Options 45 | cabalOptions options = 46 | options { 47 | optNixpkgsIdentifier = \i -> Just (binding # (i, path # ["pkgs", i])) 48 | } 49 | 50 | -- | Copied (and modified) from src/Distribution/Nixpkgs/Meta.hs 51 | fromCabalPlatform :: Platform -> String 52 | fromCabalPlatform (Platform I386 Linux) = "i686-linux" 53 | fromCabalPlatform (Platform X86_64 Linux) = "x86_64-linux" 54 | fromCabalPlatform (Platform X86_64 OSX) = "x86_64-darwin" 55 | fromCabalPlatform (Platform AArch64 Linux)= "aarch64-linux" 56 | fromCabalPlatform p = error ("fromCabalPlatform: invalid Nix platform" ++ show p) 57 | -------------------------------------------------------------------------------- /stack2nix.cabal: -------------------------------------------------------------------------------- 1 | name: stack2nix 2 | version: 0.2.3 3 | synopsis: Convert stack.yaml files into Nix build instructions. 4 | description: Convert stack.yaml files into Nix build instructions. 5 | license: MIT 6 | license-file: LICENSE 7 | author: IOHK DevOps 8 | maintainer: stack2nix@iohk.io 9 | category: Distribution, Nix 10 | build-type: Simple 11 | extra-source-files: 12 | README.md 13 | ChangeLog.md 14 | cabal-version: >= 1.10 15 | 16 | source-repository head 17 | type: git 18 | location: https://github.com/input-output-hk/stack2nix.git 19 | 20 | library 21 | hs-source-dirs: src 22 | build-depends: base >=4.9 && <4.13 23 | , Cabal >= 2.0.0.2 && < 2.5 24 | , async >= 2.1.1.1 && < 2.3 25 | , bytestring 26 | , cabal2nix >= 2.10 27 | , containers >= 0.5.7.1 && < 0.7 28 | , directory >= 1.3 && < 1.4 29 | , distribution-nixpkgs >= 1.1 && < 1.3 30 | , filepath >= 1.4.1.1 && < 1.5 31 | , hackage-db 32 | , optparse-applicative >= 0.13.2 && < 0.15 33 | , pretty 34 | , path 35 | , language-nix 36 | , lens 37 | , process >= 1.4.3 && < 1.7 38 | , regex-pcre >= 0.94.4 && < 0.95 39 | , SafeSemaphore >= 0.10.1 && < 0.11 40 | , stack >= 1.9 41 | , temporary >= 1.2.0.4 && < 1.4 42 | , text >= 1.2.2.1 && < 1.3 43 | , time 44 | exposed-modules: Stack2nix 45 | , Stack2nix.PP 46 | , Stack2nix.Render 47 | , Stack2nix.Types 48 | , Stack2nix.Util 49 | other-modules: Stack2nix.External.Cabal2nix 50 | , Stack2nix.External.Stack 51 | , Stack2nix.External.VCS.Git 52 | , Stack2nix.External.Util 53 | , Stack2nix.Hackage 54 | , Paths_stack2nix 55 | ghc-options: -Wall 56 | default-language: Haskell2010 57 | 58 | executable stack2nix 59 | main-is: Main.hs 60 | build-depends: base 61 | , Cabal 62 | , stack2nix 63 | , optparse-applicative 64 | , time 65 | hs-source-dirs: stack2nix 66 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 67 | default-language: Haskell2010 68 | 69 | test-suite test 70 | default-language: 71 | Haskell2010 72 | type: 73 | exitcode-stdio-1.0 74 | hs-source-dirs: 75 | tests 76 | main-is: 77 | test.hs 78 | build-depends: 79 | base >= 4 && < 5 80 | , hspec 81 | , stack2nix 82 | -------------------------------------------------------------------------------- /src/Stack2nix.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NamedFieldPuns #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | module Stack2nix 6 | ( Args(..) 7 | , stack2nix 8 | , version 9 | ) where 10 | 11 | import Control.Monad (unless, void, when) 12 | import Data.Maybe (isJust) 13 | import Data.Monoid ((<>)) 14 | import Paths_stack2nix (version) 15 | import Stack2nix.External.Stack 16 | import Stack2nix.External.Util (runCmdFrom, failHard) 17 | import Stack2nix.External.VCS.Git (Command (..), ExternalCmd (..), 18 | InternalCmd (..), git) 19 | import Stack2nix.Types (Args (..)) 20 | import Stack2nix.Util 21 | import System.Directory (doesFileExist, 22 | getCurrentDirectory, withCurrentDirectory) 23 | import System.Environment (getEnv, setEnv) 24 | import System.FilePath (()) 25 | import System.IO.Temp (withSystemTempDirectory) 26 | 27 | stack2nix :: Args -> IO () 28 | stack2nix args@Args{..} = do 29 | when argEnsureExecutables $ do 30 | ensureExecutableExists "cabal" "cabal-install" 31 | ensureExecutableExists "git" "git" 32 | ensureExecutableExists "nix-prefetch-git" "nix-prefetch-scripts" 33 | assertMinVer "git" "2" 34 | assertMinVer "cabal" "2" 35 | setEnv "GIT_QUIET" "y" 36 | updateCabalPackageIndex 37 | -- cwd <- getCurrentDirectory 38 | -- let projRoot = if isAbsolute argUri then argUri else cwd argUri 39 | let projRoot = argUri 40 | isLocalRepo <- doesFileExist $ projRoot argStackYaml 41 | logDebug args $ "stack2nix (isLocalRepo): " ++ show isLocalRepo 42 | logDebug args $ "stack2nix (projRoot): " ++ show projRoot 43 | logDebug args $ "stack2nix (argUri): " ++ show argUri 44 | if isLocalRepo 45 | then handleStackConfig Nothing projRoot 46 | else withSystemTempDirectory "s2n-" $ \tmpDir -> 47 | tryGit tmpDir >> handleStackConfig (Just argUri) tmpDir 48 | where 49 | updateCabalPackageIndex :: IO () 50 | updateCabalPackageIndex = do 51 | home <- getEnv "HOME" 52 | out <- runCmdFrom home "cabal" ["update"] 53 | void $ failHard out 54 | 55 | tryGit :: FilePath -> IO () 56 | tryGit tmpDir = do 57 | void $ git $ OutsideRepo $ Clone argUri tmpDir 58 | case argRev of 59 | Just r -> void $ git $ InsideRepo tmpDir (Checkout r) 60 | Nothing -> return mempty 61 | 62 | handleStackConfig :: Maybe String -> FilePath -> IO () 63 | handleStackConfig remoteUri localDir = do 64 | cwd <- getCurrentDirectory 65 | logDebug args $ "handleStackConfig (cwd): " ++ cwd 66 | logDebug args $ "handleStackConfig (localDir): " ++ localDir 67 | logDebug args $ "handleStackConfig (remoteUri): " ++ show remoteUri 68 | let stackFile = localDir argStackYaml 69 | alreadyExists <- doesFileExist stackFile 70 | unless alreadyExists $ error $ stackFile <> " does not exist. Use 'stack init' to create it." 71 | logDebug args $ "handleStackConfig (alreadyExists): " ++ show alreadyExists 72 | let go = if isJust remoteUri 73 | then withCurrentDirectory localDir 74 | else id 75 | go $ runPlan localDir remoteUri args 76 | -------------------------------------------------------------------------------- /src/Stack2nix/Util.hs: -------------------------------------------------------------------------------- 1 | module Stack2nix.Util 2 | ( assertMinVer 3 | , extractVersion 4 | , mapPool 5 | , logDebug 6 | , ensureExecutableExists 7 | , ensureExecutable 8 | ) where 9 | 10 | import Control.Concurrent.Async 11 | import Control.Concurrent.MSem 12 | import Control.Exception (onException) 13 | import Control.Monad (unless) 14 | import Data.Maybe (listToMaybe) 15 | import qualified Data.Traversable as T 16 | import Data.Version (Version (..), parseVersion, 17 | showVersion) 18 | import Data.Text (pack, strip, unpack) 19 | import GHC.Exts (sortWith) 20 | import Stack2nix.External.Util (runCmd) 21 | import Stack2nix.Types (Args, argVerbose) 22 | import System.Directory (findExecutable) 23 | import System.Environment (getEnv, setEnv) 24 | import System.Exit (ExitCode (..)) 25 | import System.IO (hPutStrLn, stderr) 26 | import System.Process (readProcessWithExitCode) 27 | import Text.ParserCombinators.ReadP (readP_to_S) 28 | import Text.Regex.PCRE (AllTextMatches (..), 29 | getAllTextMatches, (=~)) 30 | 31 | -- Credit: https://stackoverflow.com/a/18898822/204305 32 | mapPool :: T.Traversable t => Int -> (a -> IO b) -> t a -> IO (t b) 33 | mapPool max' f xs = do 34 | sem <- new max' 35 | mapConcurrently (with sem . f) xs 36 | 37 | -- heuristic for parsing version from stdout 38 | extractVersion :: String -> Maybe Version 39 | extractVersion str = ver 40 | where 41 | firstLine = head . lines $ str 42 | candidateVers = getAllTextMatches (firstLine =~ "[\\d\\.]+" :: AllTextMatches [] String) 43 | bestMatch = head . reverse . sortWith length $ candidateVers 44 | ver = fmap fst . listToMaybe . reverse . readP_to_S parseVersion $ bestMatch 45 | 46 | assertMinVer :: String -> String -> IO () 47 | assertMinVer prog minVer = do 48 | hPutStrLn stderr $ unwords ["Ensuring", prog, "version is >=", minVer, "..."] 49 | result <- runCmd prog ["--version"] `onException` error ("Failed to run " ++ prog ++ ". Not found in PATH.") 50 | case result of 51 | (ExitSuccess, out, _) -> 52 | let ver = extractVersion out in 53 | unless (ver >= extractVersion minVer) $ error $ unwords ["ERROR:", prog, "version must be", minVer, "or higher. Current version:", maybe "[parse failure]" showVersion ver] 54 | (ExitFailure _, _, err) -> error err 55 | 56 | logDebug :: Args -> String -> IO () 57 | logDebug args msg 58 | | argVerbose args = hPutStrLn stderr msg 59 | | otherwise = return () 60 | 61 | 62 | -- check if executable is present, if not provision it with nix 63 | ensureExecutableExists :: String -> String -> IO () 64 | ensureExecutableExists executable nixAttr = do 65 | exec <- findExecutable executable 66 | case exec of 67 | Just _ -> return () 68 | Nothing -> ensureExecutable nixAttr 69 | 70 | -- given nixAttr, build it and add $out/bin to $PATH 71 | ensureExecutable :: String -> IO () 72 | ensureExecutable nixAttr = do 73 | (exitcode2, stdout, err2) <- readProcessWithExitCode "nix-build" ["-A", nixAttr, "", "--no-build-output", "--no-out-link"] mempty 74 | case exitcode2 of 75 | ExitSuccess -> do 76 | hPutStrLn stderr $ err2 77 | path <- getEnv "PATH" 78 | setEnv "PATH" (unpack (strip (pack stdout)) ++ "/bin" ++ ":" ++ path) 79 | ExitFailure _ -> error $ nixAttr ++ " failed to build via nix:\n" ++ err2 80 | -------------------------------------------------------------------------------- /stack2nix/Main.hs: -------------------------------------------------------------------------------- 1 | module Main ( main ) where 2 | 3 | import Data.Semigroup ((<>)) 4 | import Data.Time (UTCTime, defaultTimeLocale, 5 | parseTimeM) 6 | import qualified Distribution.Compat.ReadP as P 7 | import Distribution.System (Arch (..), OS (..), Platform (..), 8 | buildPlatform) 9 | import Distribution.Text (display) 10 | import Options.Applicative 11 | import Stack2nix 12 | import System.IO (BufferMode (..), hSetBuffering, 13 | stderr, stdout) 14 | 15 | args :: Parser Args 16 | args = Args 17 | <$> optional (strOption $ long "revision" <> help "revision to use when fetching from VCS") 18 | <*> optional (strOption $ short 'o' <> help "output file for generated nix expression" <> metavar "PATH") 19 | <*> strOption (long "stack-yaml" <> help "Override project stack.yaml file" <> showDefault <> value "stack.yaml") 20 | <*> option auto (short 'j' <> help "number of threads for subprocesses" <> showDefault <> value 4 <> metavar "INT") 21 | <*> switch (long "test" <> help "enable tests") 22 | <*> switch (long "bench" <> help "enable benchmarks") 23 | <*> switch (long "haddock" <> help "enable documentation generation") 24 | <*> optional (option utcTimeReader (long "hackage-snapshot" <> help "hackage snapshot time, ISO format")) 25 | <*> option (readP platformReader) (long "platform" <> help "target platform to use when invoking stack or cabal2nix" <> value buildPlatform <> showDefaultWith display) 26 | <*> strArgument (metavar "URI") 27 | <*> flag True False (long "no-indent" <> help "disable indentation and place one item per line") 28 | <*> switch (long "verbose" <> help "verbose output") 29 | <*> optional (strOption $ long "cabal2nix-args" <> help "extra arguments for cabal2nix") 30 | <*> flag True False (long "no-ensure-executables" <> help "do not ensure required executables are installed") 31 | where 32 | -- | A parser for the date. Hackage updates happen maybe once or twice a month. 33 | -- Example: parseTime defaultTimeLocale "%FT%T%QZ" "2017-11-20T12:18:35Z" :: Maybe UTCTime 34 | utcTimeReader :: ReadM UTCTime 35 | utcTimeReader = eitherReader $ \arg -> 36 | case parseTimeM True defaultTimeLocale "%FT%T%QZ" arg of 37 | Nothing -> Left $ "Cannot parse date, ISO format used ('2017-11-20T12:18:35Z'): " ++ arg 38 | Just utcTime -> Right utcTime 39 | 40 | -- | A String parser for Distribution.System.Platform 41 | -- | Copied from cabal2nix/src/Cabal2nix.hs 42 | platformReader :: P.ReadP r Platform 43 | platformReader = do 44 | arch <- P.choice 45 | [ P.string "i686" >> return I386 46 | , P.string "x86_64" >> return X86_64 47 | ] 48 | _ <- P.char '-' 49 | os <- P.choice 50 | [ P.string "linux" >> return Linux 51 | , P.string "osx" >> return OSX 52 | , P.string "darwin" >> return OSX 53 | ] 54 | return (Platform arch os) 55 | 56 | readP :: P.ReadP a a -> ReadM a 57 | readP p = eitherReader $ \s -> 58 | case [ r' | (r',"") <- P.readP_to_S p s ] of 59 | (r:_) -> Right r 60 | _ -> Left ("invalid value " ++ show s) 61 | 62 | 63 | main :: IO () 64 | main = do 65 | hSetBuffering stdout LineBuffering 66 | hSetBuffering stderr LineBuffering 67 | stack2nix =<< execParser opts 68 | where 69 | opts = info 70 | (helper 71 | <*> infoOption ("stack2nix " ++ display version) (long "version" <> help "Show version number") 72 | <*> args) $ 73 | fullDesc 74 | <> progDesc "Generate a nix expression for a Haskell package using stack" 75 | -------------------------------------------------------------------------------- /.stylish-haskell.yaml: -------------------------------------------------------------------------------- 1 | # stylish-haskell configuration file 2 | # ================================== 3 | 4 | # The stylish-haskell tool is mainly configured by specifying steps. These steps 5 | # are a list, so they have an order, and one specific step may appear more than 6 | # once (if needed). Each file is processed by these steps in the given order. 7 | steps: 8 | # Convert some ASCII sequences to their Unicode equivalents. This is disabled 9 | # by default. 10 | # - unicode_syntax: 11 | # # In order to make this work, we also need to insert the UnicodeSyntax 12 | # # language pragma. If this flag is set to true, we insert it when it's 13 | # # not already present. You may want to disable it if you configure 14 | # # language extensions using some other method than pragmas. Default: 15 | # # true. 16 | # add_language_pragma: true 17 | 18 | # Align the right hand side of some elements. This is quite conservative 19 | # and only applies to statements where each element occupies a single 20 | # line. 21 | - simple_align: 22 | cases: true 23 | top_level_patterns: true 24 | records: true 25 | 26 | # Import cleanup 27 | - imports: 28 | # There are different ways we can align names and lists. 29 | # 30 | # - global: Align the import names and import list throughout the entire 31 | # file. 32 | # 33 | # - file: Like global, but don't add padding when there are no qualified 34 | # imports in the file. 35 | # 36 | # - group: Only align the imports per group (a group is formed by adjacent 37 | # import lines). 38 | # 39 | # - none: Do not perform any alignment. 40 | # 41 | # Default: global. 42 | align: global 43 | 44 | # Folowing options affect only import list alignment. 45 | # 46 | # List align has following options: 47 | # 48 | # - after_alias: Import list is aligned with end of import including 49 | # 'as' and 'hiding' keywords. 50 | # 51 | # > import qualified Data.List as List (concat, foldl, foldr, head, 52 | # > init, last, length) 53 | # 54 | # - with_alias: Import list is aligned with start of alias or hiding. 55 | # 56 | # > import qualified Data.List as List (concat, foldl, foldr, head, 57 | # > init, last, length) 58 | # 59 | # - new_line: Import list starts always on new line. 60 | # 61 | # > import qualified Data.List as List 62 | # > (concat, foldl, foldr, head, init, last, length) 63 | # 64 | # Default: after_alias 65 | list_align: after_alias 66 | 67 | # Long list align style takes effect when import is too long. This is 68 | # determined by 'columns' setting. 69 | # 70 | # - inline: This option will put as much specs on same line as possible. 71 | # 72 | # - new_line: Import list will start on new line. 73 | # 74 | # - new_line_multiline: Import list will start on new line when it's 75 | # short enough to fit to single line. Otherwise it'll be multiline. 76 | # 77 | # - multiline: One line per import list entry. 78 | # Type with contructor list acts like single import. 79 | # 80 | # > import qualified Data.Map as M 81 | # > ( empty 82 | # > , singleton 83 | # > , ... 84 | # > , delete 85 | # > ) 86 | # 87 | # Default: inline 88 | long_list_align: inline 89 | 90 | # Align empty list (importing instances) 91 | # 92 | # Empty list align has following options 93 | # 94 | # - inherit: inherit list_align setting 95 | # 96 | # - right_after: () is right after the module name: 97 | # 98 | # > import Vector.Instances () 99 | # 100 | # Default: inherit 101 | empty_list_align: inherit 102 | 103 | # List padding determines indentation of import list on lines after import. 104 | # This option affects 'long_list_align'. 105 | # 106 | # - : constant value 107 | # 108 | # - module_name: align under start of module name. 109 | # Useful for 'file' and 'group' align settings. 110 | list_padding: 4 111 | 112 | # Separate lists option affects formating of import list for type 113 | # or class. The only difference is single space between type and list 114 | # of constructors, selectors and class functions. 115 | # 116 | # - true: There is single space between Foldable type and list of it's 117 | # functions. 118 | # 119 | # > import Data.Foldable (Foldable (fold, foldl, foldMap)) 120 | # 121 | # - false: There is no space between Foldable type and list of it's 122 | # functions. 123 | # 124 | # > import Data.Foldable (Foldable(fold, foldl, foldMap)) 125 | # 126 | # Default: true 127 | separate_lists: true 128 | 129 | # Language pragmas 130 | - language_pragmas: 131 | # We can generate different styles of language pragma lists. 132 | # 133 | # - vertical: Vertical-spaced language pragmas, one per line. 134 | # 135 | # - compact: A more compact style. 136 | # 137 | # - compact_line: Similar to compact, but wrap each line with 138 | # `{-#LANGUAGE #-}'. 139 | # 140 | # Default: vertical. 141 | style: vertical 142 | 143 | # Align affects alignment of closing pragma brackets. 144 | # 145 | # - true: Brackets are aligned in same collumn. 146 | # 147 | # - false: Brackets are not aligned together. There is only one space 148 | # between actual import and closing bracket. 149 | # 150 | # Default: true 151 | align: true 152 | 153 | # stylish-haskell can detect redundancy of some language pragmas. If this 154 | # is set to true, it will remove those redundant pragmas. Default: true. 155 | remove_redundant: true 156 | 157 | # Replace tabs by spaces. This is disabled by default. 158 | - tabs: 159 | # Number of spaces to use for each tab. Default: 8, as specified by the 160 | # Haskell report. 161 | spaces: 8 162 | 163 | # Remove trailing whitespace 164 | - trailing_whitespace: {} 165 | 166 | # A common setting is the number of columns (parts of) code will be wrapped 167 | # to. Different steps take this into account. Default: 80. 168 | columns: 80 169 | 170 | # By default, line endings are converted according to the OS. You can override 171 | # preferred format here. 172 | # 173 | # - native: Native newline format. CRLF on Windows, LF on other OSes. 174 | # 175 | # - lf: Convert to LF ("\n"). 176 | # 177 | # - crlf: Convert to CRLF ("\r\n"). 178 | # 179 | # Default: native. 180 | newline: native 181 | 182 | # Sometimes, language extensions are specified in a cabal file or from the 183 | # command line instead of using language pragmas in the file. stylish-haskell 184 | # needs to be aware of these, so it can parse the file correctly. 185 | # 186 | # No language extensions are enabled by default. 187 | # language_extensions: 188 | # - TemplateHaskell 189 | # - QuasiQuotes 190 | -------------------------------------------------------------------------------- /src/Stack2nix/Render.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE RankNTypes #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | 6 | module Stack2nix.Render 7 | (render) where 8 | 9 | import Control.Lens 10 | import Control.Monad (when) 11 | import qualified Data.ByteString as BS 12 | import Data.Either (lefts, rights) 13 | import Data.List (filter, isPrefixOf, 14 | sort) 15 | import Data.Monoid ((<>)) 16 | import Data.Set (Set) 17 | import qualified Data.Set as Set 18 | import qualified Data.Text as Text 19 | import Data.Text.Encoding (encodeUtf8) 20 | import Distribution.Nixpkgs.Haskell.BuildInfo (haskell, pkgconfig, 21 | system, tool) 22 | import Distribution.Nixpkgs.Haskell.Derivation (Derivation, 23 | benchmarkDepends, 24 | dependencies, doCheck, 25 | pkgid, runHaddock, 26 | testDepends) 27 | import Distribution.Text (display) 28 | import Distribution.Types.PackageId (PackageIdentifier (..), 29 | pkgName) 30 | import Distribution.Types.PackageName (unPackageName) 31 | import Language.Nix (path) 32 | import Language.Nix.Binding (Binding, reference) 33 | import Language.Nix.PrettyPrinting (disp) 34 | import Paths_stack2nix (version) 35 | import Stack2nix.Types (Args (..)) 36 | import Stack2nix.PP (ppIndented, ppSingletons) 37 | import System.IO (hPutStrLn, stderr) 38 | import qualified Text.PrettyPrint as PP 39 | import Text.PrettyPrint.HughesPJClass (Doc, fcat, nest, 40 | pPrint, punctuate, 41 | semi, space, text) 42 | 43 | -- Boot packages (wired-in and non-wired-in). 44 | -- These are set to `null` in the generated nix package set. 45 | -- The wired-in packages follow 46 | -- * https://github.com/commercialhaskell/stack/blob/d8e942ea69eb189f67a045f0c595612034dbb75d/src/Stack/Constants.hs#L102 47 | -- * https://downloads.haskell.org/~ghc/7.10.1/docs/html/libraries/ghc/src/Module.html#integerPackageKey 48 | -- For recent GHC releases: 49 | -- * https://github.com/ghc/ghc/blob/ghc-8.2.2-release/compiler/basicTypes/Module.hs#L1073 50 | -- * https://github.com/ghc/ghc/blob/ghc-8.4.4-release/compiler/basicTypes/Module.hs#L1078 51 | -- * https://github.com/ghc/ghc/blob/ghc-8.6.4-release/compiler/basicTypes/Module.hs#L1066 (got rid of "dph-seq" and "dph-par") 52 | -- * https://github.com/ghc/ghc/blob/334dd6da47326f47b/compiler/basicTypes/Module.hs#L1088 (in-progress 8.8) 53 | -- TODO: This should probably be dependent on the GHC version used. 54 | -- A split into wired-in and not-wired-in packages may also be advisable. 55 | basePackages :: Set String 56 | basePackages = Set.fromList 57 | [ "array" 58 | , "base" 59 | -- bin-package-db is in GHC 7.10's boot libraries 60 | , "bin-package-db" 61 | , "binary" 62 | , "bytestring" 63 | , "Cabal" 64 | , "containers" 65 | , "deepseq" 66 | , "directory" 67 | , "dph-par" -- for GHC < 8.6 68 | , "dph-seq" -- for GHC < 8.6 69 | , "filepath" 70 | , "ghc" 71 | , "ghc-boot" 72 | , "ghc-boot-th" 73 | , "ghc-prim" 74 | , "ghci" 75 | , "haskeline" 76 | , "hoopl" 77 | , "hpc" 78 | , "integer-gmp" -- for GHC < 8.8 79 | , "integer-simple" -- for GHC < 8.8 80 | , "integer-wired-in" -- for GHC >= 8.8, see https://gitlab.haskell.org/ghc/ghc/commit/fc2ff6dd7496a33bf68165b28f37f40b7d647418 81 | , "interactive" 82 | , "pretty" 83 | , "process" 84 | , "rts" 85 | , "template-haskell" 86 | , "terminfo" 87 | , "time" 88 | , "transformers" 89 | , "unix" 90 | , "xhtml" 91 | ] 92 | 93 | render :: [Either Doc Derivation] -> Args -> [String] -> String -> IO () 94 | render results args locals ghcnixversion = do 95 | let docs = lefts results 96 | when (length docs > 0) $ do 97 | hPutStrLn stderr $ show docs 98 | error "Error(s) happened during cabal2nix generation ^^" 99 | let drvs = rights results 100 | 101 | -- See what base packages are missing in the derivations list and null them 102 | let missing = sort $ Set.toList $ Set.difference basePackages $ Set.fromList (map drvToName drvs) 103 | let renderedMissing = map (\b -> nest 6 (text (b <> " = null;"))) missing 104 | let pp = if argIndent args then ppIndented else ppSingletons 105 | 106 | let out = defaultNix pp ghcnixversion $ renderedMissing ++ map (renderOne args locals) drvs 107 | 108 | case argOutFile args of 109 | Just fname -> BS.writeFile fname (encodeUtf8 $ Text.pack out) 110 | Nothing -> putStrLn out 111 | 112 | renderOne :: Args -> [String] -> Derivation -> Doc 113 | renderOne args locals drv' = nest 6 $ PP.hang 114 | (PP.doubleQuotes (text pid) <> " = callPackage") 115 | 2 116 | ("(" <> pPrint drv <> ") {" <> text (show pkgs) <> "};") 117 | where 118 | pid = drvToName drv 119 | deps = view dependencies drv 120 | nixPkgs :: [Binding] 121 | nixPkgs = Set.toList $ Set.union (view pkgconfig deps) (view system deps) 122 | -- filter out libX stuff to prevent breakage in generated set 123 | nonXpkgs = filter 124 | (\e -> not 125 | ( "libX" 126 | `Data.List.isPrefixOf` (display (((view (reference . path) e) !! 1))) 127 | ) 128 | ) 129 | nixPkgs 130 | pkgs = fcat $ punctuate space [ disp b <> semi | b <- nonXpkgs ] 131 | drv = 132 | filterDepends args isLocal drv' 133 | & doCheck 134 | .~ (argTest args && isLocal) 135 | & runHaddock 136 | .~ (argHaddock args && isLocal) 137 | isLocal = elem pid locals 138 | 139 | filterDepends :: Args -> Bool -> Derivation -> Derivation 140 | filterDepends args isLocal drv = drv & foldr 141 | (.) 142 | id 143 | (do 144 | (depend, predicate) <- 145 | [(Lens testDepends, argTest args), (Lens benchmarkDepends, argBench args)] 146 | binding <- [Lens haskell, Lens pkgconfig, Lens system, Lens tool] 147 | pure 148 | $ runLens depend 149 | . runLens binding 150 | .~ (if predicate && isLocal 151 | then view (runLens depend . runLens binding) drv 152 | else Set.empty 153 | ) 154 | ) 155 | 156 | drvToName :: Derivation -> String 157 | drvToName drv = unPackageName $ pkgName $ view pkgid drv 158 | 159 | defaultNix :: (Doc -> String) -> String -> [Doc] -> String 160 | defaultNix pp ghcnixversion drvs = unlines $ 161 | [ "# Generated using stack2nix " <> display version <> "." 162 | , "" 163 | , "{ pkgs ? (import {})" 164 | , ", compiler ? pkgs.haskell.packages.ghc" ++ ghcnixversion 165 | , "}:" 166 | , "" 167 | , "with pkgs.haskell.lib;" 168 | , "" 169 | , "let" 170 | , " stackPackages = { pkgs, stdenv, callPackage }:" 171 | , " self: {" 172 | ] ++ (map pp drvs) ++ 173 | [ " };" 174 | , "in compiler.override {" 175 | , " initialPackages = stackPackages;" 176 | , " configurationCommon = { ... }: self: super: {};" 177 | , " compilerConfig = self: super: {};" 178 | , "}" 179 | ] 180 | -------------------------------------------------------------------------------- /src/Stack2nix/External/Stack.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | module Stack2nix.External.Stack 6 | ( PackageRef(..), runPlan 7 | ) where 8 | 9 | import Control.Lens ((%~)) 10 | import Control.Monad (when) 11 | import Data.List (concat) 12 | import qualified Data.Map.Strict as M 13 | import Data.Maybe (fromJust) 14 | import qualified Data.Set as Set (fromList, 15 | union) 16 | import Data.Text (pack, unpack) 17 | import Distribution.Nixpkgs.Haskell.Derivation (Derivation, 18 | configureFlags) 19 | import qualified Distribution.Nixpkgs.Haskell.Hackage as DB 20 | import Options.Applicative 21 | import Path (parseAbsFile) 22 | import Stack.Build.Source (getGhcOptions, loadSourceMapFull) 23 | import Stack.Build.Target (NeedTargets (..)) 24 | import Stack.Config 25 | import Stack.Options.BuildParser 26 | import Stack.Options.GlobalParser 27 | import Stack.Options.Utils (GlobalOptsContext (..)) 28 | import Stack.Prelude hiding 29 | (logDebug) 30 | import Stack.Runners (loadCompilerVersion, 31 | withBuildConfig) 32 | import Stack.Types.BuildPlan (PackageLocation (..), 33 | Repo (..)) 34 | import Stack.Types.Compiler (getGhcVersion) 35 | import Stack.Types.Config 36 | import Stack.Types.Config.Build (BuildCommand (..)) 37 | import Stack.Types.FlagName (toCabalFlagName) 38 | import Stack.Types.Nix 39 | import Stack.Types.Package (PackageSource (..), 40 | lpLocation, 41 | lpPackage, 42 | packageFlags, 43 | packageName, 44 | packageVersion) 45 | import Stack.Types.PackageIdentifier (PackageIdentifier (..), 46 | PackageIdentifierRevision (..), 47 | packageIdentifierString) 48 | import Stack.Types.PackageName (PackageName, parsePackageName) 49 | import Stack.Types.Runner 50 | import Stack.Types.Version (Version) 51 | import Stack2nix.External.Cabal2nix (cabal2nix) 52 | import Stack2nix.Hackage (loadHackageDB) 53 | import Stack2nix.Render (render) 54 | import Stack2nix.Types (Args (..), Flags) 55 | import Stack2nix.Util (ensureExecutable, 56 | logDebug, 57 | mapPool) 58 | import System.Directory (canonicalizePath, 59 | createDirectoryIfMissing, 60 | getCurrentDirectory, 61 | makeRelativeToCurrentDirectory) 62 | import System.FilePath (makeRelative, 63 | ()) 64 | import Text.PrettyPrint.HughesPJClass (Doc) 65 | 66 | data PackageRef 67 | = HackagePackage Flags PackageIdentifierRevision 68 | | NonHackagePackage Flags PackageIdentifier (PackageLocation FilePath) 69 | deriving (Eq, Show) 70 | 71 | genNixFile :: Args -> Version -> FilePath -> Maybe String -> Maybe String -> DB.HackageDB -> PackageRef -> IO (Either Doc Derivation) 72 | genNixFile args ghcVersion baseDir uri argRev hackageDB pkgRef = do 73 | cwd <- getCurrentDirectory 74 | case pkgRef of 75 | NonHackagePackage _flags _ident PLArchive {} -> error "genNixFile: No support for archive package locations" 76 | HackagePackage flags (PackageIdentifierRevision pkg _) -> 77 | cabal2nix args ghcVersion ("cabal://" <> packageIdentifierString pkg) Nothing Nothing flags hackageDB 78 | NonHackagePackage flags _ident (PLRepo repo) -> 79 | cabal2nix args ghcVersion (unpack $ repoUrl repo) (Just $ repoCommit repo) (Just (repoSubdirs repo)) flags hackageDB 80 | NonHackagePackage flags _ident (PLFilePath path) -> do 81 | relPath <- makeRelativeToCurrentDirectory path 82 | projRoot <- canonicalizePath $ cwd baseDir 83 | let defDir = baseDir makeRelative projRoot path 84 | cabal2nix args ghcVersion (fromMaybe defDir uri) (pack <$> argRev) (const relPath <$> uri) flags hackageDB 85 | 86 | -- TODO: remove once we use flags, options 87 | sourceMapToPackages :: Map PackageName PackageSource -> [PackageRef] 88 | sourceMapToPackages = map sourceToPackage . M.elems 89 | where 90 | sourceToPackage :: PackageSource -> PackageRef 91 | sourceToPackage (PSIndex _ flags _options pir) = HackagePackage (toCabalFlags flags) pir 92 | sourceToPackage (PSFiles lp _) = 93 | let pkg = lpPackage lp 94 | ident = PackageIdentifier (packageName pkg) (packageVersion pkg) 95 | in NonHackagePackage (toCabalFlags $ packageFlags pkg) ident (lpLocation lp) 96 | toCabalFlags fs = [ (toCabalFlagName f0, enabled) 97 | | (f0, enabled) <- M.toList fs ] 98 | 99 | 100 | planAndGenerate 101 | :: HasEnvConfig env 102 | => BuildOptsCLI 103 | -> FilePath 104 | -> Maybe String 105 | -> Args 106 | -> Version 107 | -> RIO env () 108 | planAndGenerate boptsCli baseDir remoteUri args@Args {..} ghcVersion = do 109 | (_targets, _mbp, _locals, _extraToBuild, sourceMap) <- loadSourceMapFull 110 | NeedTargets 111 | boptsCli 112 | 113 | -- Stackage lists bin-package-db but it's in GHC 7.10's boot libraries 114 | binPackageDb <- parsePackageName "bin-package-db" 115 | let pkgs = sourceMapToPackages (M.delete binPackageDb sourceMap) 116 | liftIO $ logDebug args $ "plan:\n" ++ show pkgs 117 | 118 | hackageDB <- liftIO $ loadHackageDB Nothing argHackageSnapshot 119 | buildConf <- envConfigBuildConfig <$> view envConfigL 120 | drvs <- liftIO $ mapPool 121 | argThreads 122 | (\p -> 123 | fmap (addGhcOptions buildConf p) 124 | <$> genNixFile args ghcVersion baseDir remoteUri argRev hackageDB p 125 | ) 126 | pkgs 127 | let locals = map (\l -> show (packageName (lpPackage l))) _locals 128 | liftIO . render drvs args locals $ nixVersion ghcVersion 129 | 130 | -- | Add ghc-options declared in stack.yaml to the nix derivation for a package 131 | -- by adding to the configureFlags attribute of the derivation 132 | addGhcOptions :: BuildConfig -> PackageRef -> Derivation -> Derivation 133 | addGhcOptions buildConf pkgRef drv = 134 | drv & configureFlags %~ (Set.union stackGhcOptions) 135 | where 136 | stackGhcOptions :: Set String 137 | stackGhcOptions = 138 | Set.fromList . map (unpack . ("--ghc-option=" <>)) $ getGhcOptions 139 | buildConf 140 | buildOpts 141 | pkgName 142 | False 143 | False 144 | pkgName :: PackageName 145 | pkgName = case pkgRef of 146 | HackagePackage _ (PackageIdentifierRevision (PackageIdentifier n _) _) -> n 147 | NonHackagePackage _ (PackageIdentifier n _) _ -> n 148 | 149 | runPlan :: FilePath 150 | -> Maybe String 151 | -> Args 152 | -> IO () 153 | runPlan baseDir remoteUri args@Args{..} = do 154 | let stackRoot = "/tmp/s2n" 155 | createDirectoryIfMissing True stackRoot 156 | let globals = globalOpts baseDir stackRoot args 157 | let stackFile = baseDir argStackYaml 158 | 159 | ghcVersion <- getGhcVersionIO globals stackFile 160 | when argEnsureExecutables $ 161 | ensureExecutable ("haskell.compiler.ghc" ++ nixVersion ghcVersion) 162 | withBuildConfig globals $ planAndGenerate buildOpts baseDir remoteUri args ghcVersion 163 | 164 | nixVersion :: Version -> String 165 | nixVersion = 166 | filter (/= '.') . show 167 | 168 | getGhcVersionIO :: GlobalOpts -> FilePath -> IO Version 169 | getGhcVersionIO go stackFile = do 170 | cp <- canonicalizePath stackFile 171 | fp <- parseAbsFile cp 172 | lc <- withRunner LevelError True False ColorAuto Nothing False $ \runner -> 173 | -- https://www.fpcomplete.com/blog/2017/07/the-rio-monad 174 | runRIO runner $ loadConfig mempty Nothing (SYLOverride fp) 175 | getGhcVersion <$> loadCompilerVersion go lc 176 | 177 | globalOpts :: FilePath -> FilePath -> Args -> GlobalOpts 178 | globalOpts currentDir stackRoot Args{..} = 179 | go { globalReExecVersion = Just "1.5.1" -- TODO: obtain from stack lib if exposed 180 | , globalConfigMonoid = 181 | (globalConfigMonoid go) 182 | { configMonoidNixOpts = mempty 183 | { nixMonoidEnable = First (Just True) 184 | } 185 | } 186 | , globalStackYaml = SYLOverride (currentDir argStackYaml) 187 | , globalLogLevel = if argVerbose then LevelDebug else LevelInfo 188 | } 189 | where 190 | pinfo = info (globalOptsParser currentDir OuterGlobalOpts (Just LevelError)) briefDesc 191 | args = concat [ ["--stack-root", stackRoot] 192 | , ["--jobs", show argThreads] 193 | , ["--test" | argTest] 194 | , ["--bench" | argBench] 195 | , ["--haddock" | argHaddock] 196 | , ["--no-install-ghc"] 197 | ] 198 | go = globalOptsFromMonoid False ColorNever . fromJust . getParseResult $ 199 | execParserPure defaultPrefs pinfo args 200 | 201 | buildOpts :: BuildOptsCLI 202 | buildOpts = fromJust . getParseResult $ execParserPure defaultPrefs (info (buildOptsParser Build) briefDesc) ["--dry-run"] 203 | --------------------------------------------------------------------------------