1
1
mirror of https://github.com/nmattia/snack.git synced 2025-01-06 04:25:30 +03:00

Merge pull request #25 from nmattia/nm-hpack

Add basic HPack support
This commit is contained in:
Nicolas Mattia 2018-07-08 13:47:25 +02:00 committed by GitHub
commit 8bef714a9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 776 additions and 416 deletions

View File

@ -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

View File

@ -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" ] ;

View File

@ -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;
}

View File

@ -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
View 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
View 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
];
};
}

View File

@ -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
View 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
View 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;
};
}

View File

@ -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;
}

View File

@ -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;
};
}

View File

@ -2,7 +2,6 @@
{ lib
, callPackage
, runCommand
, singleOut
, haskellPackages
}:

View File

@ -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

View File

@ -0,0 +1,8 @@
name: snack-extensions-test
dependencies:
- text
executable:
main: Main.hs
source-dirs: .
default-extensions:
- OverloadedStrings

View File

@ -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
View 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

View File

@ -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

View 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

View File

@ -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

View File

@ -0,0 +1,6 @@
name: snack-packages-test
dependencies:
- conduit
executable:
main: Foo.hs
source-dirs: src

View File

@ -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