mirror of
https://github.com/nmattia/snack.git
synced 2025-01-06 04:25:30 +03:00
commit
8bef714a9a
308
bin/Snack.hs
308
bin/Snack.hs
@ -1,44 +1,49 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE DerivingStrategies #-}
|
||||
{-# LANGUAGE MonadFailDesugaring #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Applicative
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Class
|
||||
import Data.Aeson ((.:))
|
||||
import Data.ByteString as BS
|
||||
import Data.Aeson (FromJSON, (.:), (.:?))
|
||||
import Data.FileEmbed (embedStringFile)
|
||||
import Data.Semigroup
|
||||
import Data.String (fromString)
|
||||
import Data.List (intercalate)
|
||||
import Data.Semigroup ((<>))
|
||||
import Data.String.Interpolate
|
||||
import Data.Text as T
|
||||
import Shelly (Sh)
|
||||
import System.Posix.Process (executeFile)
|
||||
import UnliftIO.Exception
|
||||
import qualified Data.Aeson as Aeson
|
||||
import qualified Data.Aeson.Types as Aeson
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.Map as Map
|
||||
import qualified Data.Text as T
|
||||
import qualified Options.Applicative as Opts
|
||||
import qualified Shelly as S
|
||||
|
||||
{-
|
||||
|
||||
TODO:
|
||||
Mode
|
||||
= HPack HPackPah
|
||||
| Standalone SnackNix
|
||||
| Discovery (Either HPackPath SnackNix)
|
||||
-}
|
||||
|
||||
data Mode
|
||||
= Standalone SnackNix -- Reads a snack.nix file
|
||||
| HPack PackageYaml
|
||||
|
||||
-- | Like a FilePath, but Nix friendly
|
||||
newtype SnackNix = SnackNix { unSnackNix :: FilePath }
|
||||
|
||||
newtype PackageYaml = PackageYaml { unPackageYaml :: FilePath }
|
||||
|
||||
mkSnackNix :: FilePath -> SnackNix
|
||||
mkSnackNix = SnackNix -- XXX: this is not nix friendly, but it's ok, because
|
||||
-- it'll be gone soon
|
||||
--
|
||||
mkPackageYaml :: FilePath -> PackageYaml
|
||||
mkPackageYaml = PackageYaml -- XXX: this is not nix friendly, but it's ok, because
|
||||
-- it'll be gone soon
|
||||
|
||||
data Command
|
||||
= Build
|
||||
@ -55,41 +60,114 @@ data Options = Options
|
||||
, command :: Command
|
||||
}
|
||||
|
||||
data Project = Project
|
||||
{ outPath :: FilePath
|
||||
, exePath :: FilePath
|
||||
}
|
||||
|
||||
instance Aeson.FromJSON Project where
|
||||
parseJSON = Aeson.withObject "project" $ \o ->
|
||||
Project <$> o .: "out_path" <*> o .: "exe_path"
|
||||
|
||||
parseMode :: Opts.Parser Mode
|
||||
parseMode =
|
||||
Opts.flag Standalone Standalone
|
||||
(Opts.long "--standalone")
|
||||
|
||||
<*> (mkSnackNix <$>
|
||||
((Standalone . mkSnackNix) <$>
|
||||
Opts.strOption
|
||||
(Opts.long "--snack-nix"
|
||||
(Opts.long "snack-nix"
|
||||
<> Opts.short 's'
|
||||
<> Opts.value "./snack.nix"
|
||||
<> Opts.metavar "PATH")
|
||||
)
|
||||
<|>
|
||||
((HPack . mkPackageYaml) <$>
|
||||
Opts.strOption
|
||||
(Opts.long "package-yaml"
|
||||
<> Opts.value "./package.yaml"
|
||||
<> Opts.short 'p'
|
||||
<> Opts.metavar "PATH")
|
||||
)
|
||||
|
||||
options :: Opts.Parser Options
|
||||
options = Options <$> parseMode <*> parseCommand
|
||||
|
||||
snackGhci :: SnackNix -> Sh ()
|
||||
snackGhci snackNix = do
|
||||
path <- S.print_stdout False $ exePath <$> snackBuildGhci snackNix
|
||||
S.print_stdout True $ run_ (fromString path) []
|
||||
newtype ModuleName = ModuleName { unModuleName :: T.Text }
|
||||
deriving newtype (Ord, Eq, Aeson.FromJSONKey)
|
||||
deriving stock Show
|
||||
|
||||
snackBuildGhci :: SnackNix -> Sh Project
|
||||
snackBuildGhci snackNix = do
|
||||
out <- runStdin1
|
||||
data BuildResult
|
||||
= BuiltLibrary LibraryBuild
|
||||
| BuiltExecutable ExecutableBuild
|
||||
| BuiltMulti MultiBuild
|
||||
| BuiltGhci GhciBuild
|
||||
deriving Show
|
||||
|
||||
instance Aeson.FromJSON BuildResult where
|
||||
parseJSON v =
|
||||
BuiltLibrary <$> (guardBuildType "library" v)
|
||||
<|> BuiltExecutable <$> (guardBuildType "executable" v)
|
||||
<|> BuiltMulti <$> (guardBuildType "multi" v)
|
||||
<|> BuiltGhci <$> (guardBuildType "ghci" v)
|
||||
where
|
||||
guardBuildType :: FromJSON a => T.Text -> Aeson.Value -> Aeson.Parser a
|
||||
guardBuildType ty = Aeson.withObject "build result" $ \o -> do
|
||||
bty <- o .: "build_type"
|
||||
guard (bty == ty)
|
||||
Aeson.parseJSON =<< o .: "result"
|
||||
|
||||
newtype GhciBuild = GhciBuild
|
||||
{ ghciExePath :: NixPath
|
||||
}
|
||||
deriving stock Show
|
||||
|
||||
instance FromJSON GhciBuild where
|
||||
parseJSON = Aeson.withObject "ghci build" $ \o ->
|
||||
GhciBuild <$> o .: "ghci_path"
|
||||
|
||||
-- The kinds of builds: library, executable, or a mix of both (currently only
|
||||
-- for HPack)
|
||||
newtype LibraryBuild = LibraryBuild
|
||||
{ unLibraryBuild :: Map.Map ModuleName NixPath }
|
||||
deriving newtype FromJSON
|
||||
deriving stock Show
|
||||
|
||||
newtype ExecutableBuild = ExecutableBuild
|
||||
{ exePath :: NixPath }
|
||||
deriving stock Show
|
||||
|
||||
instance FromJSON ExecutableBuild where
|
||||
parseJSON = Aeson.withObject "executable build" $ \o ->
|
||||
ExecutableBuild <$> o .: "exe_path"
|
||||
|
||||
data MultiBuild = MultiBuild
|
||||
{ librayBuild :: Maybe LibraryBuild
|
||||
, executableBuilds :: Map.Map T.Text ExecutableBuild
|
||||
}
|
||||
deriving stock Show
|
||||
|
||||
instance Aeson.FromJSON MultiBuild where
|
||||
parseJSON = Aeson.withObject "multi build" $ \o ->
|
||||
MultiBuild
|
||||
<$> o .:? "library"
|
||||
<*> o .: "executables"
|
||||
|
||||
data NixArg = NixArg
|
||||
{ argType :: NixArgType
|
||||
, argName :: T.Text
|
||||
, argValue :: T.Text
|
||||
}
|
||||
|
||||
data NixArgType
|
||||
= ArgStr
|
||||
| Arg
|
||||
|
||||
newtype NixExpr = NixExpr { unNixExpr :: T.Text }
|
||||
|
||||
newtype NixPath = NixPath { unNixPath :: T.Text }
|
||||
deriving newtype FromJSON
|
||||
deriving stock Show
|
||||
|
||||
decodeOrFail :: FromJSON a => BS.ByteString -> Sh a
|
||||
decodeOrFail bs = case Aeson.decodeStrict' bs of
|
||||
Just foo -> pure foo
|
||||
Nothing -> throwIO $ userError $ unlines
|
||||
[ "could not decode " <> show bs ]
|
||||
|
||||
nixBuild :: [NixArg] -> NixExpr -> Sh NixPath
|
||||
nixBuild extraNixArgs nixExpr =
|
||||
NixPath <$> runStdin1
|
||||
(T.pack [i|
|
||||
{ snackNix, lib64, specJson }:
|
||||
{ #{ intercalate "," funArgs } }:
|
||||
let
|
||||
spec = builtins.fromJSON specJson;
|
||||
pkgs = import (builtins.fetchTarball
|
||||
@ -108,76 +186,103 @@ snackBuildGhci snackNix = do
|
||||
chmod +w $out
|
||||
'';
|
||||
snack = pkgs.callPackage libDir {};
|
||||
proj = snack.executable (import snackNix);
|
||||
in
|
||||
{ build = proj.build;
|
||||
ghci = proj.ghci;
|
||||
}
|
||||
|]
|
||||
)
|
||||
in #{ T.unpack $ unNixExpr $ nixExpr }
|
||||
|])
|
||||
"nix-build"
|
||||
[ "-"
|
||||
, "--arg", "snackNix", T.pack $ unSnackNix snackNix
|
||||
, "--argstr", "lib64", libb64
|
||||
, "--argstr", "specJson", specJson
|
||||
, "--no-out-link"
|
||||
, "-A", "ghci.json"
|
||||
]
|
||||
json <- liftIO $ BS.readFile (T.unpack out)
|
||||
let Just proj = Aeson.decodeStrict' json
|
||||
pure proj
|
||||
cliArgs
|
||||
where
|
||||
cliArgs :: [T.Text]
|
||||
cliArgs =
|
||||
[ "-" -- read expression from stdin
|
||||
, "--no-out-link" -- no need for roots
|
||||
] <> (concatMap toCliArgs nixArgs)
|
||||
funArgs :: [String]
|
||||
funArgs = toFunArg <$> nixArgs
|
||||
nixArgs :: [NixArg]
|
||||
nixArgs =
|
||||
[ NixArg { argType = ArgStr , argName = "specJson", argValue = specJson }
|
||||
, NixArg { argType = ArgStr , argName = "lib64", argValue = libb64 }
|
||||
] <> extraNixArgs
|
||||
toFunArg :: NixArg -> String
|
||||
toFunArg = T.unpack . argName
|
||||
toCliArgs :: NixArg -> [T.Text]
|
||||
toCliArgs narg = case argType narg of
|
||||
{ Arg -> "--arg"; ArgStr -> "--argstr" }
|
||||
: [ argName narg , argValue narg ]
|
||||
|
||||
snackBuild :: SnackNix -> Sh Project
|
||||
snackBuild :: SnackNix -> Sh BuildResult
|
||||
snackBuild snackNix = do
|
||||
out <- runStdin1
|
||||
(T.pack [i|
|
||||
{ snackNix, lib64, specJson }:
|
||||
let
|
||||
spec = builtins.fromJSON specJson;
|
||||
pkgs = import (builtins.fetchTarball
|
||||
{ url = "https://github.com/${spec.owner}/${spec.repo}/archive/${spec.rev}.tar.gz";
|
||||
sha256 = spec.sha256;
|
||||
}) {} ;
|
||||
libDir =
|
||||
let
|
||||
b64 = pkgs.writeTextFile { name = "lib-b64"; text = lib64; };
|
||||
in
|
||||
pkgs.runCommand "snack-lib" {}
|
||||
''
|
||||
cat ${b64} | base64 --decode > out.tar.gz
|
||||
mkdir -p $out
|
||||
tar -C $out -xzf out.tar.gz
|
||||
chmod +w $out
|
||||
'';
|
||||
snack = pkgs.callPackage libDir {};
|
||||
proj = snack.executable (import snackNix);
|
||||
in
|
||||
{ build = proj.build;
|
||||
ghci = proj.ghci;
|
||||
NixPath out <- nixBuild
|
||||
[ NixArg
|
||||
{ argName = "snackNix"
|
||||
, argValue = T.pack $ unSnackNix snackNix
|
||||
, argType = Arg
|
||||
}
|
||||
|]
|
||||
)
|
||||
"nix-build"
|
||||
[ "-"
|
||||
, "--arg", "snackNix", T.pack $ unSnackNix snackNix
|
||||
, "--argstr", "lib64", libb64
|
||||
, "--argstr", "specJson", specJson
|
||||
, "--no-out-link"
|
||||
, "-A", "build.json"
|
||||
]
|
||||
json <- liftIO $ BS.readFile (T.unpack out)
|
||||
let Just proj = Aeson.decodeStrict' json
|
||||
pure proj
|
||||
$ NixExpr "snack.inferSnackBuild snackNix"
|
||||
decodeOrFail =<< liftIO (BS.readFile $ T.unpack out)
|
||||
|
||||
snackGhci :: SnackNix -> Sh GhciBuild
|
||||
snackGhci snackNix = do
|
||||
NixPath out <- nixBuild
|
||||
[ NixArg
|
||||
{ argName = "snackNix"
|
||||
, argValue = T.pack $ unSnackNix snackNix
|
||||
, argType = Arg
|
||||
}
|
||||
]
|
||||
$ NixExpr "snack.inferSnackGhci snackNix"
|
||||
liftIO (BS.readFile (T.unpack out)) >>= decodeOrFail >>= \case
|
||||
BuiltGhci g -> pure g
|
||||
b -> throwIO $ userError $ "Expected GHCi build, got " <> show b
|
||||
|
||||
snackBuildHPack :: PackageYaml -> Sh BuildResult
|
||||
snackBuildHPack packageYaml = do
|
||||
NixPath out <- nixBuild
|
||||
[ NixArg
|
||||
{ argName = "packageYaml"
|
||||
, argValue = T.pack $ unPackageYaml packageYaml
|
||||
, argType = Arg
|
||||
}
|
||||
]
|
||||
$ NixExpr "snack.inferHPackBuild packageYaml"
|
||||
decodeOrFail =<< liftIO (BS.readFile (T.unpack out))
|
||||
|
||||
snackGhciHPack :: PackageYaml -> Sh GhciBuild
|
||||
snackGhciHPack packageYaml = do
|
||||
NixPath out <- nixBuild
|
||||
[ NixArg
|
||||
{ argName = "packageYaml"
|
||||
, argValue = T.pack $ unPackageYaml packageYaml
|
||||
, argType = Arg
|
||||
}
|
||||
]
|
||||
$ NixExpr "snack.inferHPackGhci packageYaml"
|
||||
liftIO (BS.readFile (T.unpack out)) >>= decodeOrFail >>= \case
|
||||
BuiltGhci g -> pure g
|
||||
b -> throwIO $ userError $ "Expected GHCi build, got " <> show b
|
||||
|
||||
runCommand :: Mode -> Command -> IO ()
|
||||
runCommand (Standalone snackNix) = \case
|
||||
Build -> S.shelly $ void $ snackBuild snackNix
|
||||
Run -> snackRun snackBuild
|
||||
Ghci -> snackRun snackBuildGhci
|
||||
where
|
||||
snackRun build = do
|
||||
fp <- S.shelly $ S.print_stdout False $ exePath <$> build snackNix
|
||||
executeFile fp True [] Nothing
|
||||
Run -> quiet (snackBuild snackNix) >>= runBuildResult
|
||||
Ghci -> runExe =<< ghciExePath <$> (quiet $ snackGhci snackNix)
|
||||
runCommand (HPack packageYaml) = \case
|
||||
Build -> S.shelly $ void $ snackBuildHPack packageYaml
|
||||
Run -> quiet (snackBuildHPack packageYaml) >>= runBuildResult
|
||||
Ghci -> runExe =<< ghciExePath <$> (quiet $ snackGhciHPack packageYaml)
|
||||
|
||||
runBuildResult :: BuildResult -> IO ()
|
||||
runBuildResult = \case
|
||||
BuiltExecutable (ExecutableBuild p) -> runExe p
|
||||
BuiltMulti b
|
||||
| [ExecutableBuild exe] <- Map.elems (executableBuilds b) -> runExe exe
|
||||
b -> fail $ "Unexpected build type: " <> show b
|
||||
|
||||
quiet :: Sh a -> IO a
|
||||
quiet = S.shelly . S.print_stdout False
|
||||
runExe :: NixPath -> IO ()
|
||||
runExe (NixPath fp) = executeFile (T.unpack fp) True [] Nothing
|
||||
|
||||
parseCommand :: Opts.Parser Command
|
||||
parseCommand =
|
||||
@ -193,8 +298,9 @@ run p args = T.lines <$> S.run p args
|
||||
runStdin1 :: T.Text -> S.FilePath -> [T.Text] -> Sh T.Text
|
||||
runStdin1 stin p args = do
|
||||
S.setStdin stin
|
||||
[out] <- run p args
|
||||
pure out
|
||||
run p args >>= \case
|
||||
[out] -> pure out
|
||||
xs -> throwIO $ userError $ "unexpected output: " <> show xs
|
||||
|
||||
run_ :: S.FilePath -> [T.Text] -> Sh ()
|
||||
run_ p args = void $ run p args
|
||||
|
@ -1,11 +1,11 @@
|
||||
{ runCommand, writeTextFile, symlinkJoin }:
|
||||
let
|
||||
specJson = writeTextFile
|
||||
pkgs = import ../nix {};
|
||||
specJson = pkgs.writeTextFile
|
||||
{ name = "spec-json";
|
||||
text = builtins.readFile ../nix/nixpkgs/nixpkgs-src.json;
|
||||
destination = "/spec.json";
|
||||
};
|
||||
lib64 = runCommand "lib64" {}
|
||||
lib64 = pkgs.runCommand "lib64" {}
|
||||
''
|
||||
tar -czf lib.tar.gz -C ${../snack-lib} .
|
||||
mkdir -p $out
|
||||
@ -23,6 +23,7 @@ in
|
||||
"shelly"
|
||||
"text"
|
||||
"unix"
|
||||
"unliftio"
|
||||
];
|
||||
ghcOpts = [ "-Werror" "-Wall" ] ;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
_: pkgs: rec {
|
||||
snack-lib = pkgs.callPackage ../snack-lib/default.nix { };
|
||||
snack-exe = (snack-lib.executable (import ../bin/snack.nix { inherit (pkgs) writeTextFile symlinkJoin runCommand;})).build.out;
|
||||
snack-exe =
|
||||
(snack-lib.buildAsExecutable (snack-lib.snackSpec ../bin/snack.nix)).out;
|
||||
}
|
||||
|
14
script/test
14
script/test
@ -1,15 +1,20 @@
|
||||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -i bash
|
||||
#!nix-shell -I nixpkgs=./nix
|
||||
#!nix-shell -p snack-exe
|
||||
#!nix-shell -p shfmt
|
||||
#!nix-shell -p git
|
||||
#!nix-shell -p jq
|
||||
#!nix-shell -p nix
|
||||
#!nix-shell -p shfmt
|
||||
#!nix-shell -p snack-exe
|
||||
#!nix-shell -p glibcLocales
|
||||
#!nix-shell --pure
|
||||
# vim: ft=sh sw=2 et
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export LC_ALL="en_US.utf-8"
|
||||
export LANG="en_US.utf-8"
|
||||
|
||||
## Functions
|
||||
|
||||
banner() {
|
||||
@ -84,6 +89,11 @@ pushd tests/extensions
|
||||
./test
|
||||
popd
|
||||
|
||||
banner "HPack"
|
||||
pushd tests/hpack
|
||||
./test
|
||||
popd
|
||||
|
||||
banner "Test this file's formatting"
|
||||
list=$(shfmt -i 2 -l script/test)
|
||||
if [[ -n "$list" ]]; then
|
||||
|
17
snack-lib/YamlToJson.hs
Normal file
17
snack-lib/YamlToJson.hs
Normal file
@ -0,0 +1,17 @@
|
||||
module Main where
|
||||
|
||||
import qualified Data.Aeson as Aeson
|
||||
import qualified Data.Yaml as Yaml
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL8
|
||||
import qualified Data.ByteString.Char8 as BS8
|
||||
import qualified Data.Text.Encoding as T
|
||||
import System.Environment (getArgs)
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
[file] <- getArgs
|
||||
yaml <- BS8.readFile file
|
||||
let Just value = Yaml.decode yaml :: Maybe Aeson.Value
|
||||
BL8.putStrLn $ Aeson.encode value
|
143
snack-lib/build.nix
Normal file
143
snack-lib/build.nix
Normal file
@ -0,0 +1,143 @@
|
||||
{ runCommand
|
||||
, lib
|
||||
, callPackage
|
||||
, stdenv
|
||||
, rsync
|
||||
, symlinkJoin
|
||||
}:
|
||||
|
||||
with (callPackage ./modules.nix {});
|
||||
with (callPackage ./lib.nix {});
|
||||
with (callPackage ./module-spec.nix {});
|
||||
|
||||
rec {
|
||||
|
||||
# Returns an attribute set where the keys are all the built module names and
|
||||
# the values are the paths to the object files.
|
||||
# mainModSpec: a "main" module
|
||||
buildMain = ghcWith: mainModSpec:
|
||||
buildModulesRec ghcWith
|
||||
# XXX: the main modules need special handling regarding the object name
|
||||
{ "${mainModSpec.moduleName}" =
|
||||
"${buildModule ghcWith mainModSpec}/Main.o";}
|
||||
mainModSpec.moduleImports;
|
||||
|
||||
# returns a attrset where the keys are the module names and the values are
|
||||
# the modules' object file path
|
||||
buildLibrary = ghcWith: modSpecs:
|
||||
buildModulesRec ghcWith {} modSpecs;
|
||||
|
||||
linkMainModule = ghcWith: mod: # main module
|
||||
let
|
||||
objAttrs = buildMain ghcWith mod;
|
||||
objList = lib.attrsets.mapAttrsToList (x: y: y) objAttrs;
|
||||
deps = allTransitiveDeps [mod];
|
||||
ghc = ghcWith deps;
|
||||
ghcOptsArgs = lib.strings.escapeShellArgs mod.moduleGhcOpts;
|
||||
packageList = map (p: "-package ${p}") deps;
|
||||
relExePath = "bin/${lib.strings.toLower mod.moduleName}";
|
||||
drv = runCommand "linker" {}
|
||||
''
|
||||
mkdir -p $out/bin
|
||||
${ghc}/bin/ghc \
|
||||
${lib.strings.escapeShellArgs packageList} \
|
||||
${lib.strings.escapeShellArgs objList} \
|
||||
${ghcOptsArgs} \
|
||||
-o $out/${relExePath}
|
||||
'';
|
||||
in
|
||||
{
|
||||
out = drv;
|
||||
relExePath = relExePath;
|
||||
};
|
||||
|
||||
# Build the given modules (recursively) using the given accumulator to keep
|
||||
# track of which modules have been built already
|
||||
# XXX: doesn't work if several modules in the DAG have the same name
|
||||
buildModulesRec = ghcWith: empty: modSpecs:
|
||||
foldDAG
|
||||
{ f = mod:
|
||||
{ "${mod.moduleName}" =
|
||||
"${buildModule ghcWith mod}/${moduleToObject mod.moduleName}";
|
||||
};
|
||||
elemLabel = mod: mod.moduleName;
|
||||
elemChildren = mod: mod.moduleImports;
|
||||
reduce = a: b: a // b;
|
||||
empty = empty;
|
||||
}
|
||||
modSpecs;
|
||||
|
||||
buildModule = ghcWith: modSpec:
|
||||
let
|
||||
ghc = ghcWith modSpec.moduleDependencies;
|
||||
exts = modSpec.moduleExtensions;
|
||||
ghcOpts = modSpec.moduleGhcOpts ++ (map (x: "-X${x}") exts);
|
||||
ghcOptsArgs = lib.strings.escapeShellArgs ghcOpts;
|
||||
objectName = modSpec.moduleName;
|
||||
builtDeps = map (buildModule ghcWith) modSpec.moduleImports;
|
||||
depsDirs = map (x: x + "/") builtDeps;
|
||||
base = modSpec.moduleBase;
|
||||
makeSymtree =
|
||||
if lib.lists.length depsDirs >= 1
|
||||
# TODO: symlink instead of copy
|
||||
then "rsync -r ${lib.strings.escapeShellArgs depsDirs} ."
|
||||
else "";
|
||||
makeSymModule =
|
||||
# TODO: symlink instead of copy
|
||||
"rsync -r ${singleOutModule base modSpec.moduleName}/ .";
|
||||
pred = file: path: type:
|
||||
let
|
||||
topLevel = (builtins.toString base) + "/";
|
||||
actual = (lib.strings.removePrefix topLevel path);
|
||||
expected = file;
|
||||
in
|
||||
(expected == actual) ||
|
||||
(type == "directory" && (lib.strings.hasPrefix actual expected));
|
||||
|
||||
extraFiles = builtins.filterSource
|
||||
(p: t:
|
||||
lib.lists.length
|
||||
(
|
||||
let
|
||||
topLevel = (builtins.toString base) + "/";
|
||||
actual = lib.strings.removePrefix topLevel p;
|
||||
in
|
||||
lib.filter (expected:
|
||||
(expected == actual) ||
|
||||
(t == "directory" && (lib.strings.hasPrefix actual expected))
|
||||
)
|
||||
modSpec.moduleFiles
|
||||
) >= 1
|
||||
) base;
|
||||
in stdenv.mkDerivation
|
||||
{ name = objectName;
|
||||
src = symlinkJoin
|
||||
{ name = "extra-files";
|
||||
paths = [ extraFiles ] ++ modSpec.moduleDirectories;
|
||||
};
|
||||
phases =
|
||||
[ "unpackPhase" "buildPhase" ];
|
||||
buildPhase =
|
||||
''
|
||||
echo "Building module ${modSpec.moduleName}"
|
||||
mkdir -p $out
|
||||
echo "Creating dependencies symtree for module ${modSpec.moduleName}"
|
||||
${makeSymtree}
|
||||
echo "Creating module symlink for module ${modSpec.moduleName}"
|
||||
${makeSymModule}
|
||||
echo "Compiling module ${modSpec.moduleName}"
|
||||
# Set a tmpdir we have control over, otherwise GHC fails, not sure why
|
||||
mkdir -p tmp
|
||||
ghc -tmpdir tmp/ ${moduleToFile modSpec.moduleName} -c \
|
||||
-outputdir $out \
|
||||
${ghcOptsArgs} \
|
||||
2>&1
|
||||
echo "Done building module ${modSpec.moduleName}"
|
||||
'';
|
||||
|
||||
buildInputs =
|
||||
[ ghc
|
||||
rsync
|
||||
];
|
||||
};
|
||||
}
|
@ -12,309 +12,155 @@
|
||||
, callPackage
|
||||
}:
|
||||
|
||||
with (callPackage ./build.nix {});
|
||||
with (callPackage ./files.nix {});
|
||||
|
||||
# why is "inherit" needed?
|
||||
with (callPackage ./modules.nix { inherit singleOut; });
|
||||
with (callPackage ./module-spec.nix { inherit singleOut; });
|
||||
with (callPackage ./package-spec.nix { inherit singleOut; });
|
||||
with (callPackage ./ghci.nix {});
|
||||
with (callPackage ./lib.nix {});
|
||||
with (callPackage ./modules.nix {});
|
||||
with (callPackage ./module-spec.nix {});
|
||||
with (callPackage ./package-spec.nix {});
|
||||
with (callPackage ./hpack.nix {});
|
||||
|
||||
let
|
||||
ghcWith = deps: haskellPackages.ghcWithPackages
|
||||
(ps: map (p: ps.${p}) deps);
|
||||
|
||||
buildModule = ghcWith: modSpec:
|
||||
# Assumes the package description describes an executable
|
||||
withMainModSpec = pkgDescr: act:
|
||||
let
|
||||
ghc = ghcWith modSpec.moduleDependencies;
|
||||
exts = modSpec.moduleExtensions;
|
||||
ghcOpts = modSpec.moduleGhcOpts ++ (map (x: "-X${x}") exts);
|
||||
ghcOptsArgs = lib.strings.escapeShellArgs ghcOpts;
|
||||
objectName = modSpec.moduleName;
|
||||
builtDeps = map (buildModule ghcWith) modSpec.moduleImports;
|
||||
depsDirs = map (x: x + "/") builtDeps;
|
||||
base = modSpec.moduleBase;
|
||||
makeSymtree =
|
||||
if lib.lists.length depsDirs >= 1
|
||||
# TODO: symlink instead of copy
|
||||
then "rsync -r ${lib.strings.escapeShellArgs depsDirs} ."
|
||||
else "";
|
||||
makeSymModule =
|
||||
# TODO: symlink instead of copy
|
||||
"rsync -r ${singleOutModule base modSpec.moduleName}/ .";
|
||||
pred = file: path: type:
|
||||
mainModName = pkgDescr.packageMain;
|
||||
mainModSpec =
|
||||
let
|
||||
topLevel = (builtins.toString base) + "/";
|
||||
actual = (lib.strings.removePrefix topLevel path);
|
||||
expected = file;
|
||||
in
|
||||
(expected == actual) ||
|
||||
(type == "directory" && (lib.strings.hasPrefix actual expected));
|
||||
|
||||
extraFiles = builtins.filterSource
|
||||
(p: t:
|
||||
lib.lists.length
|
||||
(
|
||||
let
|
||||
topLevel = (builtins.toString base) + "/";
|
||||
actual = lib.strings.removePrefix topLevel p;
|
||||
in
|
||||
lib.filter (expected:
|
||||
(expected == actual) ||
|
||||
(t == "directory" && (lib.strings.hasPrefix actual expected))
|
||||
)
|
||||
modSpec.moduleFiles
|
||||
) >= 1
|
||||
) base;
|
||||
in stdenv.mkDerivation
|
||||
{ name = objectName;
|
||||
src = symlinkJoin
|
||||
{ name = "extra-files";
|
||||
paths = [ extraFiles ] ++ modSpec.moduleDirectories;
|
||||
};
|
||||
phases =
|
||||
[ "unpackPhase" "buildPhase" ];
|
||||
buildPhase =
|
||||
''
|
||||
echo "Building module ${modSpec.moduleName}"
|
||||
mkdir -p $out
|
||||
echo "Creating dependencies symtree for module ${modSpec.moduleName}"
|
||||
${makeSymtree}
|
||||
echo "Creating module symlink for module ${modSpec.moduleName}"
|
||||
${makeSymModule}
|
||||
echo "Compiling module ${modSpec.moduleName}"
|
||||
# Set a tmpdir we have control over, otherwise GHC fails, not sure why
|
||||
mkdir -p tmp
|
||||
ghc -tmpdir tmp/ ${moduleToFile modSpec.moduleName} -c \
|
||||
-outputdir $out \
|
||||
${ghcOptsArgs} \
|
||||
2>&1
|
||||
echo "Done building module ${modSpec.moduleName}"
|
||||
'';
|
||||
|
||||
buildInputs =
|
||||
[ ghc
|
||||
rsync
|
||||
];
|
||||
};
|
||||
|
||||
# Returns an attribute set where the keys are all the built module names and
|
||||
# the values are the paths to the object files.
|
||||
# mainModSpec: a "main" module
|
||||
buildMain = ghcWith: mainModSpec:
|
||||
buildModulesRec ghcWith
|
||||
# XXX: the main modules need special handling regarding the object name
|
||||
{ "${mainModSpec.moduleName}" =
|
||||
"${buildModule ghcWith mainModSpec}/Main.o";}
|
||||
mainModSpec.moduleImports;
|
||||
|
||||
# returns a attrset where the keys are the module names and the values are
|
||||
# the modules' object file path
|
||||
buildLibrary = ghcWith: modSpecs:
|
||||
buildModulesRec ghcWith {} modSpecs;
|
||||
|
||||
# Build the given modules (recursively) using the given accumulator to keep
|
||||
# track of which modules have been built already
|
||||
# XXX: doesn't work if several modules in the DAG have the same name
|
||||
buildModulesRec = ghcWith: empty: modSpecs:
|
||||
foldDAG
|
||||
{ f = mod:
|
||||
{ "${mod.moduleName}" =
|
||||
"${buildModule ghcWith mod}/${moduleToObject mod.moduleName}";
|
||||
};
|
||||
elemLabel = mod: mod.moduleName;
|
||||
elemChildren = mod: mod.moduleImports;
|
||||
reduce = a: b: a // b;
|
||||
empty = empty;
|
||||
}
|
||||
modSpecs;
|
||||
|
||||
linkMainModule = ghcWith: mod: # main module
|
||||
let
|
||||
objAttrs = buildMain ghcWith mod;
|
||||
objList = lib.attrsets.mapAttrsToList (x: y: y) objAttrs;
|
||||
deps = allTransitiveDeps [mod];
|
||||
ghc = ghcWith deps;
|
||||
ghcOptsArgs = lib.strings.escapeShellArgs mod.moduleGhcOpts;
|
||||
packageList = map (p: "-package ${p}") deps;
|
||||
relExePath = "bin/${lib.strings.toLower mod.moduleName}";
|
||||
drv = runCommand "linker" {}
|
||||
''
|
||||
mkdir -p $out/bin
|
||||
${ghc}/bin/ghc \
|
||||
${lib.strings.escapeShellArgs packageList} \
|
||||
${lib.strings.escapeShellArgs objList} \
|
||||
${ghcOptsArgs} \
|
||||
-o $out/${relExePath}
|
||||
'';
|
||||
fld = moduleSpecFold' modSpecs;
|
||||
modSpecs = foldDAG fld [mainModName];
|
||||
in modSpecs.${mainModName};
|
||||
drv = linkMainModule ghcWith mainModSpec;
|
||||
in
|
||||
{
|
||||
out = drv;
|
||||
relExePath = relExePath;
|
||||
{ out = drv.out;
|
||||
outPath = "${drv.out}";
|
||||
exePath = "${drv.out}/${drv.relExePath}";
|
||||
};
|
||||
|
||||
|
||||
# Write a new ghci executable that loads all the modules defined in the
|
||||
# module spec
|
||||
ghciWithMain = ghcWith: mainModSpec:
|
||||
libraryModSpecs = pkgSpec:
|
||||
let
|
||||
imports = allTransitiveImports [mainModSpec];
|
||||
modSpecs = [mainModSpec] ++ imports;
|
||||
in ghciWithModules ghcWith modSpecs;
|
||||
moduleSpecFold' = modSpecFoldFromPackageSpec pkgSpec;
|
||||
modNames = listModulesInDir pkgSpec.packageBase;
|
||||
fld = moduleSpecFold' modSpecs';
|
||||
modSpecs' = foldDAG fld modNames;
|
||||
modSpecs = builtins.attrValues modSpecs';
|
||||
in modSpecs;
|
||||
|
||||
ghciWithModules = ghcWith: modSpecs:
|
||||
executableMainModSpec = pkgSpec:
|
||||
let
|
||||
ghcOpts = allTransitiveGhcOpts modSpecs
|
||||
++ (map (x: "-X${x}") (allTransitiveExtensions modSpecs));
|
||||
ghc = ghcWith (allTransitiveDeps modSpecs);
|
||||
ghciArgs = lib.strings.escapeShellArgs
|
||||
(ghcOpts ++ absoluteModuleFiles);
|
||||
absoluteModuleFiles =
|
||||
map
|
||||
(mod:
|
||||
builtins.toString (mod.moduleBase) +
|
||||
"/${moduleToFile mod.moduleName}"
|
||||
)
|
||||
modSpecs;
|
||||
moduleSpecFold' = modSpecFoldFromPackageSpec pkgSpec;
|
||||
mainModName = pkgSpec.packageMain;
|
||||
mainModSpec =
|
||||
let
|
||||
fld = moduleSpecFold' modSpecs;
|
||||
modSpecs = foldDAG fld [mainModName];
|
||||
in modSpecs.${mainModName};
|
||||
in mainModSpec;
|
||||
|
||||
dirs = allTransitiveDirectories modSpecs;
|
||||
newGhc =
|
||||
symlinkJoin
|
||||
{ name = "ghci";
|
||||
paths = [ ghc ];
|
||||
postBuild =
|
||||
''
|
||||
wrapProgram "$out/bin/ghci" \
|
||||
--add-flags "${ghciArgs}"
|
||||
'';
|
||||
buildInputs = [makeWrapper];
|
||||
};
|
||||
buildAsLibrary = pkgSpec:
|
||||
buildLibrary ghcWith (libraryModSpecs pkgSpec);
|
||||
|
||||
buildAsExecutable = pkgSpec:
|
||||
let drv = linkMainModule ghcWith (executableMainModSpec pkgSpec);
|
||||
in
|
||||
# This symlinks the extra dirs to $PWD for GHCi to work
|
||||
writeScriptBin "ghci-with-files"
|
||||
''
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
{ out = drv.out;
|
||||
exe_path = "${drv.out}/${drv.relExePath}";
|
||||
};
|
||||
|
||||
TRAPS=""
|
||||
for i in ${lib.strings.escapeShellArgs dirs}; do
|
||||
if [ "$i" != "$PWD" ]; then
|
||||
for j in $(find "$i" ! -path "$i"); do
|
||||
file=$(basename $j)
|
||||
echo "Temporarily symlinking $j to $file..."
|
||||
ln -s $j $file
|
||||
TRAPS="rm $file ; $TRAPS"
|
||||
trap "$TRAPS" EXIT
|
||||
echo "done."
|
||||
done
|
||||
fi
|
||||
done
|
||||
${newGhc}/bin/ghci
|
||||
'';
|
||||
|
||||
# Takes a package spec and returns (modSpecs -> Fold)
|
||||
modSpecFoldFromPackageSpec = pkgSpec:
|
||||
let
|
||||
baseByModuleName = modName:
|
||||
let res = pkgSpecByModuleName pkgSpec null modName;
|
||||
in if res == null then null else res.packageBase;
|
||||
depsByModuleName = modName:
|
||||
(pkgSpecByModuleName
|
||||
pkgSpec
|
||||
(abort "asking dependencies for external module: ${modName}")
|
||||
modName).packageDependencies
|
||||
modName
|
||||
;
|
||||
extsByModuleName = modName:
|
||||
(pkgSpecByModuleName
|
||||
pkgSpec
|
||||
(abort "asking extensions for external module: ${modName}")
|
||||
modName).packageExtensions;
|
||||
ghcOptsByModuleName = modName:
|
||||
(pkgSpecByModuleName
|
||||
pkgSpec
|
||||
(abort "asking ghc options for external module: ${modName}")
|
||||
modName).packageGhcOpts;
|
||||
in
|
||||
moduleSpecFold
|
||||
{ baseByModuleName = baseByModuleName;
|
||||
filesByModuleName = pkgSpec.packageExtraFiles;
|
||||
dirsByModuleName = pkgSpec.packageExtraDirectories;
|
||||
depsByModuleName = depsByModuleName;
|
||||
extsByModuleName = extsByModuleName;
|
||||
ghcOptsByModuleName = ghcOptsByModuleName;
|
||||
};
|
||||
|
||||
# TODO: "executable" is a bad name
|
||||
executable = pkgDescr:
|
||||
inferSnackBuild = snackNix: writeText "snack-build-json"
|
||||
( builtins.toJSON (
|
||||
let
|
||||
moduleSpecFold' = modSpecFoldFromPackageSpec topPkgSpec;
|
||||
topPkgSpec = mkPackageSpec pkgDescr;
|
||||
ghcWith = deps: haskellPackages.ghcWithPackages
|
||||
(ps: map (p: ps.${p}) deps);
|
||||
pkgSpec = mkPackageSpec (import snackNix);
|
||||
in
|
||||
if builtins.isNull topPkgSpec.packageMain
|
||||
if builtins.isNull pkgSpec.packageMain
|
||||
then
|
||||
let
|
||||
modNames = listModulesInDir topPkgSpec.packageBase;
|
||||
fld = moduleSpecFold' modSpecs';
|
||||
modSpecs' = foldDAG fld modNames;
|
||||
modSpecs = builtins.attrValues modSpecs';
|
||||
in
|
||||
{
|
||||
build =
|
||||
# This json is a bit different than the other ones, because it's
|
||||
# a map of modules to object files (rather than out_path +
|
||||
# exe_path)
|
||||
{ json = writeText "build_output"
|
||||
(builtins.toJSON (buildLibrary ghcWith modSpecs));
|
||||
};
|
||||
ghci =
|
||||
let
|
||||
drv = ghciWithModules ghcWith modSpecs;
|
||||
json =
|
||||
{ out_path = "${drv.out}";
|
||||
exe_path = "${drv.out}/bin/ghci-with-files";
|
||||
};
|
||||
in
|
||||
{ out = drv.out;
|
||||
json = writeText "ghci_output" (builtins.toJSON json);
|
||||
};
|
||||
}
|
||||
{ "build_type" = "library";
|
||||
"result" = buildAsLibrary pkgSpec;
|
||||
}
|
||||
else
|
||||
let
|
||||
mainModName = topPkgSpec.packageMain;
|
||||
mainModSpec =
|
||||
let
|
||||
fld = moduleSpecFold' modSpecs;
|
||||
modSpecs = foldDAG fld [mainModName];
|
||||
in modSpecs.${mainModName};
|
||||
in
|
||||
{ build =
|
||||
let
|
||||
drv = linkMainModule ghcWith mainModSpec;
|
||||
json =
|
||||
{ out_path = "${drv.out}";
|
||||
exe_path = "${drv.out}/${drv.relExePath}";
|
||||
};
|
||||
in
|
||||
{ out = drv.out;
|
||||
json = writeText "build_output" (builtins.toJSON json);
|
||||
};
|
||||
{ "build_type" = "executable";
|
||||
"result" = buildAsExecutable pkgSpec;
|
||||
}
|
||||
));
|
||||
|
||||
ghci =
|
||||
let
|
||||
drv = ghciWithMain ghcWith mainModSpec;
|
||||
json =
|
||||
{ out_path = "${drv.out}";
|
||||
exe_path = "${drv.out}/bin/ghci-with-files";
|
||||
};
|
||||
in
|
||||
{ out = drv.out;
|
||||
json = writeText "ghci_output" (builtins.toJSON json);
|
||||
};
|
||||
inferSnackGhci = snackNix: writeText "snack-ghci-json"
|
||||
( builtins.toJSON (
|
||||
let
|
||||
pkgSpec = mkPackageSpec (import snackNix);
|
||||
drv =
|
||||
if builtins.isNull pkgSpec.packageMain
|
||||
then ghciWithModules ghcWith (libraryModSpecs pkgSpec)
|
||||
else ghciWithMain ghcWith (executableMainModSpec pkgSpec);
|
||||
in
|
||||
{ build_type = "ghci";
|
||||
result = {
|
||||
"ghci_path" = "${drv.out}/bin/ghci-with-files";
|
||||
};
|
||||
}
|
||||
));
|
||||
|
||||
inferHPackBuild = packageYaml: writeText "hpack-build-json"
|
||||
( builtins.toJSON (
|
||||
let pkgSpecs = hpackSpecs packageYaml;
|
||||
in
|
||||
{ build_type = "multi";
|
||||
result =
|
||||
{ library =
|
||||
if builtins.isNull pkgSpecs.library
|
||||
then null
|
||||
else buildAsLibrary (pkgSpecs.library);
|
||||
executables = lib.attrsets.mapAttrs (k: v: buildAsExecutable v) pkgSpecs.executables;
|
||||
};
|
||||
}
|
||||
));
|
||||
|
||||
inferHPackGhci = packageYaml: writeText "hpack-ghci-json"
|
||||
( builtins.toJSON (
|
||||
let
|
||||
pkgSpecs = hpackSpecs packageYaml;
|
||||
pkgSpec = mkPackageSpec (import snackNix);
|
||||
drv =
|
||||
let exeSpecs = builtins.attrValues pkgSpecs.executables;
|
||||
in
|
||||
if lib.lists.length exeSpecs == 1
|
||||
then ghciWithMain ghcWith (executableMainModSpec (lib.lists.head exeSpecs))
|
||||
else
|
||||
if builtins.isNull pkgSpecs.library
|
||||
then abort "GHCi: needs either a single executable or a library"
|
||||
else ghciWithModules ghcWith (libraryModSpecs pkgSpecs.library);
|
||||
in
|
||||
{ build_type = "ghci";
|
||||
result = {
|
||||
"ghci_path" = "${drv.out}/bin/ghci-with-files";
|
||||
};
|
||||
}
|
||||
));
|
||||
|
||||
snackSpec = snackNix: mkPackageSpec (import snackNix);
|
||||
hpackSpecs = packageYaml:
|
||||
let
|
||||
descrs = pkgDescrsFromHPack packageYaml;
|
||||
in
|
||||
{ library = withAttr descrs "library" null
|
||||
(comp: if builtins.isNull comp then null else mkPackageSpec comp);
|
||||
executables =
|
||||
lib.attrsets.mapAttrs (k: v: mkPackageSpec v) descrs.executables;
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit
|
||||
executable
|
||||
inferSnackBuild
|
||||
inferSnackGhci
|
||||
inferHPackBuild
|
||||
inferHPackGhci
|
||||
packageYaml
|
||||
buildAsExecutable
|
||||
buildAsLibrary
|
||||
snackSpec
|
||||
hpackSpec
|
||||
;
|
||||
}
|
||||
|
65
snack-lib/ghci.nix
Normal file
65
snack-lib/ghci.nix
Normal file
@ -0,0 +1,65 @@
|
||||
{ makeWrapper, symlinkJoin, lib, callPackage, writeScriptBin }:
|
||||
|
||||
with (callPackage ./module-spec.nix {});
|
||||
with (callPackage ./modules.nix {});
|
||||
|
||||
rec {
|
||||
|
||||
# Write a new ghci executable that loads all the modules defined in the
|
||||
# module spec
|
||||
ghciWithMain = ghcWith: mainModSpec:
|
||||
let
|
||||
imports = allTransitiveImports [mainModSpec];
|
||||
modSpecs = [mainModSpec] ++ imports;
|
||||
in ghciWithModules ghcWith modSpecs;
|
||||
|
||||
ghciWithModules = ghcWith: modSpecs:
|
||||
let
|
||||
ghcOpts = allTransitiveGhcOpts modSpecs
|
||||
++ (map (x: "-X${x}") (allTransitiveExtensions modSpecs));
|
||||
ghc = ghcWith (allTransitiveDeps modSpecs);
|
||||
ghciArgs = lib.strings.escapeShellArgs
|
||||
(ghcOpts ++ absoluteModuleFiles);
|
||||
absoluteModuleFiles =
|
||||
map
|
||||
(mod:
|
||||
builtins.toString (mod.moduleBase) +
|
||||
"/${moduleToFile mod.moduleName}"
|
||||
)
|
||||
modSpecs;
|
||||
|
||||
dirs = allTransitiveDirectories modSpecs;
|
||||
newGhc =
|
||||
symlinkJoin
|
||||
{ name = "ghci";
|
||||
paths = [ ghc ];
|
||||
postBuild =
|
||||
''
|
||||
wrapProgram "$out/bin/ghci" \
|
||||
--add-flags "${ghciArgs}"
|
||||
'';
|
||||
buildInputs = [makeWrapper];
|
||||
};
|
||||
in
|
||||
# This symlinks the extra dirs to $PWD for GHCi to work
|
||||
writeScriptBin "ghci-with-files"
|
||||
''
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TRAPS=""
|
||||
for i in ${lib.strings.escapeShellArgs dirs}; do
|
||||
if [ "$i" != "$PWD" ]; then
|
||||
for j in $(find "$i" ! -path "$i"); do
|
||||
file=$(basename $j)
|
||||
echo "Temporarily symlinking $j to $file..."
|
||||
ln -s $j $file
|
||||
TRAPS="rm $file ; $TRAPS"
|
||||
trap "$TRAPS" EXIT
|
||||
echo "done."
|
||||
done
|
||||
fi
|
||||
done
|
||||
${newGhc}/bin/ghci
|
||||
'';
|
||||
}
|
67
snack-lib/hpack.nix
Normal file
67
snack-lib/hpack.nix
Normal file
@ -0,0 +1,67 @@
|
||||
{ lib, glibcLocales, callPackage, writeText, runCommand, haskellPackages }:
|
||||
|
||||
with (callPackage ./lib.nix {});
|
||||
with (callPackage ./modules.nix {});
|
||||
|
||||
let
|
||||
y2j = runCommand "yaml2json"
|
||||
|
||||
{ buildInputs =
|
||||
[ (haskellPackages.ghcWithPackages (ps: [ ps.aeson ps.yaml ])) glibcLocales ];
|
||||
}
|
||||
"ghc ${./YamlToJson.hs} -o $out";
|
||||
|
||||
fromYAML = text:
|
||||
let json =
|
||||
|
||||
builtins.readFile (runCommand "y2j"
|
||||
{ buildInputs = [ glibcLocales ]; }
|
||||
"${y2j} ${writeText "y2j" text} > $out"
|
||||
);
|
||||
in builtins.fromJSON json;
|
||||
in
|
||||
{
|
||||
# Returns an attribute set with two fields:
|
||||
# - library: a package spec
|
||||
# - executable: an attr set of executable name to package spec
|
||||
pkgDescrsFromHPack = packageYaml:
|
||||
let
|
||||
package = fromYAML (builtins.readFile packageYaml);
|
||||
topDeps =
|
||||
# this drops the version bounds
|
||||
map (x: lib.lists.head (lib.strings.splitString " " x))
|
||||
package.dependencies;
|
||||
topExtensions = optAttr package "default-extensions" [];
|
||||
packageLib = withAttr package "library" null (component:
|
||||
{ src =
|
||||
let base = builtins.dirOf packageYaml;
|
||||
in builtins.toPath "${builtins.toString base}/${component.source-dirs}";
|
||||
dependencies = topDeps ++ (optAttr component "dependencies" []);
|
||||
extensions = topExtensions ++ (optAttr component "extensions" []);
|
||||
}
|
||||
);
|
||||
|
||||
exes =
|
||||
withAttr package "executables" {} (lib.mapAttrs (k: v: mkExe v)) //
|
||||
withAttr package "executable" {} (comp: { ${package.name} = mkExe comp; });
|
||||
mkExe = component:
|
||||
let
|
||||
depOrPack =
|
||||
lib.lists.partition
|
||||
(x: x == package.name)
|
||||
(optAttr component "dependencies" []);
|
||||
in
|
||||
{ main = fileToModule component.main;
|
||||
src =
|
||||
let
|
||||
base = builtins.dirOf packageYaml;
|
||||
in builtins.toPath "${builtins.toString base}/${component.source-dirs}";
|
||||
dependencies = topDeps ++ depOrPack.wrong;
|
||||
extensions = topExtensions ++ (optAttr component "extensions" []);
|
||||
packages = map (_: packageLib) depOrPack.right;
|
||||
};
|
||||
in
|
||||
{ library = packageLib;
|
||||
executables = exes;
|
||||
};
|
||||
}
|
@ -43,4 +43,11 @@ foldDAGRec =
|
||||
};
|
||||
in foldDAGRec fld acc' children;
|
||||
in lib.foldl insert acc0 roots;
|
||||
|
||||
withAttr = obj: attrName: def: f:
|
||||
if builtins.hasAttr attrName obj then f (obj.${attrName}) else def;
|
||||
|
||||
optAttr = obj: attrName: def:
|
||||
if builtins.hasAttr attrName obj then obj.${attrName} else def;
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
# Functions related to module specs
|
||||
{ lib
|
||||
, callPackage
|
||||
, singleOut
|
||||
}:
|
||||
|
||||
with (callPackage ./modules.nix { inherit singleOut; });
|
||||
with (callPackage ./modules.nix {});
|
||||
with (callPackage ./package-spec.nix {});
|
||||
with (callPackage ./lib.nix {});
|
||||
|
||||
rec {
|
||||
@ -99,4 +99,39 @@ rec {
|
||||
}
|
||||
modSpecs
|
||||
);
|
||||
|
||||
|
||||
# Takes a package spec and returns (modSpecs -> Fold)
|
||||
modSpecFoldFromPackageSpec = pkgSpec:
|
||||
let
|
||||
baseByModuleName = modName:
|
||||
let res = pkgSpecByModuleName pkgSpec null modName;
|
||||
in if res == null then null else res.packageBase;
|
||||
depsByModuleName = modName:
|
||||
(pkgSpecByModuleName
|
||||
pkgSpec
|
||||
(abort "asking dependencies for external module: ${modName}")
|
||||
modName).packageDependencies
|
||||
modName
|
||||
;
|
||||
extsByModuleName = modName:
|
||||
(pkgSpecByModuleName
|
||||
pkgSpec
|
||||
(abort "asking extensions for external module: ${modName}")
|
||||
modName).packageExtensions;
|
||||
ghcOptsByModuleName = modName:
|
||||
(pkgSpecByModuleName
|
||||
pkgSpec
|
||||
(abort "asking ghc options for external module: ${modName}")
|
||||
modName).packageGhcOpts;
|
||||
in
|
||||
moduleSpecFold
|
||||
{ baseByModuleName = baseByModuleName;
|
||||
filesByModuleName = pkgSpec.packageExtraFiles;
|
||||
dirsByModuleName = pkgSpec.packageExtraDirectories;
|
||||
depsByModuleName = depsByModuleName;
|
||||
extsByModuleName = extsByModuleName;
|
||||
ghcOptsByModuleName = ghcOptsByModuleName;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
{ lib
|
||||
, callPackage
|
||||
, runCommand
|
||||
, singleOut
|
||||
, haskellPackages
|
||||
}:
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
{ lib
|
||||
, singleOut
|
||||
, callPackage
|
||||
}:
|
||||
|
||||
with (callPackage ./modules.nix { inherit singleOut; });
|
||||
with (callPackage ./modules.nix {});
|
||||
|
||||
rec {
|
||||
|
||||
@ -34,7 +33,7 @@ rec {
|
||||
if builtins.isList attr
|
||||
then (_: attr)
|
||||
else if builtins.isAttrs attr
|
||||
then (x: attr.${x})
|
||||
then (x: if builtins.hasAttr x attr then attr.${x} else [])
|
||||
else if builtins.isFunction attr
|
||||
then attr
|
||||
else
|
||||
|
8
tests/extensions/package.yaml
Normal file
8
tests/extensions/package.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
name: snack-extensions-test
|
||||
dependencies:
|
||||
- text
|
||||
executable:
|
||||
main: Main.hs
|
||||
source-dirs: .
|
||||
default-extensions:
|
||||
- OverloadedStrings
|
@ -3,12 +3,18 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
snack build
|
||||
snack run | diff golden -
|
||||
test() {
|
||||
$SNACK build
|
||||
$SNACK run | diff golden -
|
||||
|
||||
TMP_FILE=$(mktemp)
|
||||
TMP_FILE=$(mktemp)
|
||||
|
||||
capture_io "$TMP_FILE" main | snack ghci
|
||||
capture_io "$TMP_FILE" main | $SNACK ghci
|
||||
|
||||
diff golden $TMP_FILE
|
||||
rm $TMP_FILE
|
||||
diff golden $TMP_FILE
|
||||
rm $TMP_FILE
|
||||
}
|
||||
|
||||
SNACK="snack" test
|
||||
SNACK="snack -s ./snack.nix" test
|
||||
SNACK="snack --package-yaml ./package.yaml" test
|
||||
|
13
tests/hpack/test
Executable file
13
tests/hpack/test
Executable file
@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# vim: ft=sh sw=2 et
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
|
||||
git clone http://github.com/2mol/pboy.git $TMP_DIR
|
||||
git -C $TMP_DIR reset --hard a2458d6984930a33a3b1972cb6d5c167d2511b06
|
||||
|
||||
snack build --package-yaml $TMP_DIR/package.yaml
|
||||
|
||||
rm -rf $TMP_DIR
|
@ -3,13 +3,18 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TMP_FILE=$(mktemp)
|
||||
test() {
|
||||
TMP_FILE=$(mktemp)
|
||||
|
||||
cat $(snack build) | jq -M 'keys' > $TMP_FILE
|
||||
cat $($SNACK build) | jq -M '.result | keys' > $TMP_FILE
|
||||
|
||||
diff golden.jq $TMP_FILE
|
||||
diff golden.jq $TMP_FILE
|
||||
|
||||
capture_io "$TMP_FILE" main | snack ghci
|
||||
diff golden $TMP_FILE
|
||||
capture_io "$TMP_FILE" main | $SNACK ghci
|
||||
diff golden $TMP_FILE
|
||||
rm $TMP_FILE
|
||||
}
|
||||
|
||||
rm $TMP_FILE
|
||||
SNACK="snack" test
|
||||
SNACK="snack -s ./snack.nix" test
|
||||
# Note: no HPack test, because HPack doesn't support multi library
|
||||
|
13
tests/library/package.yaml
Normal file
13
tests/library/package.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
name: snack-library-test
|
||||
|
||||
dependencies:
|
||||
- conduit
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
|
||||
executable:
|
||||
main: Foo.hs
|
||||
source-dirs: app
|
||||
dependencies:
|
||||
- snack-library-test
|
@ -3,12 +3,18 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
snack build
|
||||
snack run | diff golden -
|
||||
test() {
|
||||
$SNACK build
|
||||
$SNACK run | diff golden -
|
||||
|
||||
TMP_FILE=$(mktemp)
|
||||
TMP_FILE=$(mktemp)
|
||||
|
||||
capture_io "$TMP_FILE" main | snack ghci
|
||||
capture_io "$TMP_FILE" main | $SNACK ghci
|
||||
|
||||
diff golden $TMP_FILE
|
||||
rm $TMP_FILE
|
||||
diff golden $TMP_FILE
|
||||
rm $TMP_FILE
|
||||
}
|
||||
|
||||
SNACK="snack" test
|
||||
SNACK="snack -s ./snack.nix" test
|
||||
SNACK="snack --package-yaml ./package.yaml" test
|
||||
|
6
tests/packages/package.yaml
Normal file
6
tests/packages/package.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
name: snack-packages-test
|
||||
dependencies:
|
||||
- conduit
|
||||
executable:
|
||||
main: Foo.hs
|
||||
source-dirs: src
|
@ -3,12 +3,19 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
snack build
|
||||
snack run | diff golden -
|
||||
|
||||
TMP_FILE=$(mktemp)
|
||||
test() {
|
||||
$SNACK build
|
||||
$SNACK run | diff golden -
|
||||
|
||||
capture_io "$TMP_FILE" main | snack ghci
|
||||
TMP_FILE=$(mktemp)
|
||||
|
||||
diff golden $TMP_FILE
|
||||
rm $TMP_FILE
|
||||
capture_io "$TMP_FILE" main | $SNACK ghci
|
||||
|
||||
diff golden $TMP_FILE
|
||||
rm $TMP_FILE
|
||||
}
|
||||
|
||||
SNACK="snack" test
|
||||
SNACK="snack -s ./snack.nix" test
|
||||
SNACK="snack --package-yaml ./package.yaml" test
|
||||
|
Loading…
Reference in New Issue
Block a user