1
1
mirror of https://github.com/nmattia/snack.git synced 2024-10-26 12:38:49 +03:00

Better HPack support

This commit is contained in:
Nicolas Mattia 2018-07-08 13:33:34 +02:00
parent 451e0d86da
commit 7788e6a39d
12 changed files with 345 additions and 188 deletions

View File

@ -1,36 +1,33 @@
{-# 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.Aeson (FromJSON, (.:), (.:?))
import Data.FileEmbed (embedStringFile)
import Data.List (intercalate)
import Data.Semigroup ((<>))
import Data.String (fromString)
import Data.String.Interpolate
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
@ -63,15 +60,6 @@ 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 =
((Standalone . mkSnackNix) <$>
@ -93,10 +81,65 @@ parseMode =
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
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
@ -111,9 +154,17 @@ data NixArgType
newtype NixExpr = NixExpr { unNixExpr :: T.Text }
newtype NixPath = NixPath { unNixPath :: T.Text }
deriving newtype FromJSON
deriving stock Show
snackBuildWith :: [NixArg] -> NixExpr -> Sh NixPath
snackBuildWith extraNixArgs nixExpr =
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|
{ #{ intercalate "," funArgs } }:
@ -159,65 +210,79 @@ snackBuildWith extraNixArgs nixExpr =
{ Arg -> "--arg"; ArgStr -> "--argstr" }
: [ argName narg , argValue narg ]
snackBuild :: SnackNix -> Sh Project
snackBuild :: SnackNix -> Sh BuildResult
snackBuild snackNix = do
NixPath out <- snackBuildWith
NixPath out <- nixBuild
[ NixArg
{ argName = "snackNix"
, argValue = T.pack $ unSnackNix snackNix
, argType = Arg
}
]
$ NixExpr "(snack.executable (import snackNix)).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)
snackBuildGhci :: SnackNix -> Sh Project
snackBuildGhci snackNix = do
NixPath out <- snackBuildWith
snackGhci :: SnackNix -> Sh GhciBuild
snackGhci snackNix = do
NixPath out <- nixBuild
[ NixArg
{ argName = "snackNix"
, argValue = T.pack $ unSnackNix snackNix
, argType = Arg
}
]
$ NixExpr "(snack.executable (import snackNix)).ghci.json"
json <- liftIO $ BS.readFile (T.unpack out)
let Just proj = Aeson.decodeStrict' json
pure proj
$ 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 Project
snackBuildHPack :: PackageYaml -> Sh BuildResult
snackBuildHPack packageYaml = do
NixPath out <- snackBuildWith
NixPath out <- nixBuild
[ NixArg
{ argName = "packageYaml"
, argValue = T.pack $ unPackageYaml packageYaml
, argType = Arg
}
]
$ NixExpr "(snack.packageYaml packageYaml).library.build.json"
json <- liftIO $ BS.readFile (T.unpack out)
let Just proj = Aeson.decodeStrict' json
pure proj
$ 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 -> snackRun snackBuildHPack
Ghci -> undefined -- snackRun snackBuildHPackGhci
where
snackRun build = do
fp <- S.shelly $ S.print_stdout False $ exePath <$> build packageYaml
executeFile fp True [] Nothing
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 =
@ -233,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

@ -22,87 +22,145 @@ with (callPackage ./package-spec.nix {});
with (callPackage ./hpack.nix {});
let
ghcWith = deps: haskellPackages.ghcWithPackages
(ps: map (p: ps.${p}) deps);
# TODO: "executable" is a bad name
executable = pkgDescr:
# Assumes the package description describes an executable
withMainModSpec = pkgDescr: act:
let
moduleSpecFold' = modSpecFoldFromPackageSpec topPkgSpec;
topPkgSpec = mkPackageSpec pkgDescr;
ghcWith = deps: haskellPackages.ghcWithPackages
(ps: map (p: ps.${p}) deps);
mainModName = pkgDescr.packageMain;
mainModSpec =
let
fld = moduleSpecFold' modSpecs;
modSpecs = foldDAG fld [mainModName];
in modSpecs.${mainModName};
drv = linkMainModule ghcWith mainModSpec;
in
if builtins.isNull topPkgSpec.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);
};
}
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);
};
{ out = drv.out;
outPath = "${drv.out}";
exePath = "${drv.out}/${drv.relExePath}";
};
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);
};
};
packageYaml = pyam:
libraryModSpecs = pkgSpec:
let
moduleSpecFold' = modSpecFoldFromPackageSpec pkgSpec;
modNames = listModulesInDir pkgSpec.packageBase;
fld = moduleSpecFold' modSpecs';
modSpecs' = foldDAG fld modNames;
modSpecs = builtins.attrValues modSpecs';
in modSpecs;
executableMainModSpec = pkgSpec:
let
moduleSpecFold' = modSpecFoldFromPackageSpec pkgSpec;
mainModName = pkgSpec.packageMain;
mainModSpec =
let
project = snackNixFromHPack pyam;
in
{ library = executable project.library;
executables = lib.attrsets.mapAttrs (_: v: executable v) project.executables;
fld = moduleSpecFold' modSpecs;
modSpecs = foldDAG fld [mainModName];
in modSpecs.${mainModName};
in mainModSpec;
buildAsLibrary = pkgSpec:
buildLibrary ghcWith (libraryModSpecs pkgSpec);
buildAsExecutable = pkgSpec:
let drv = linkMainModule ghcWith (executableMainModSpec pkgSpec);
in
{ out = drv.out;
exe_path = "${drv.out}/${drv.relExePath}";
};
inferSnackBuild = snackNix: writeText "snack-build-json"
( builtins.toJSON (
let
pkgSpec = mkPackageSpec (import snackNix);
in
if builtins.isNull pkgSpec.packageMain
then
{ "build_type" = "library";
"result" = buildAsLibrary pkgSpec;
}
else
{ "build_type" = "executable";
"result" = buildAsExecutable pkgSpec;
}
));
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
;
}

View File

@ -1,5 +1,6 @@
{ lib, glibcLocales, callPackage, writeText, runCommand, haskellPackages }:
with (callPackage ./lib.nix {});
with (callPackage ./modules.nix {});
let
@ -20,49 +21,44 @@ let
in builtins.fromJSON json;
in
{
snackNixFromHPack = packageYaml:
# 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;
extensions = package.default-extensions;
packageLib =
let component = package.library;
in
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 ++
(if builtins.hasAttr "dependencies" component
then component.dependencies
else []);
inherit extensions;
};
dependencies = topDeps ++ (optAttr component "dependencies" []);
extensions = topExtensions ++ (optAttr component "extensions" []);
}
);
exes =
if builtins.hasAttr "executables" package
then lib.mapAttrs (k: v: mkExe v) package.executables
else {};
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)
(if builtins.hasAttr "dependencies" component
then component.dependencies
else []);
packages = map (_: packageLib) depOrPack.right;
dependencies = topDeps ++ depOrPack.wrong;
(optAttr component "dependencies" []);
in
{ main = fileToModule component.main;
src =
let
base = builtins.dirOf packageYaml;
in builtins.toPath "${builtins.toString base}/${component.source-dirs}";
inherit packages dependencies extensions;
dependencies = topDeps ++ depOrPack.wrong;
extensions = topExtensions ++ (optAttr component "extensions" []);
packages = map (_: packageLib) depOrPack.right;
};
in
{ library = packageLib;

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

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

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

@ -10,22 +10,12 @@ test() {
TMP_FILE=$(mktemp)
capture_io "$TMP_FILE" main | snack ghci
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
#snack build
#snack run | diff golden -
#TMP_FILE=$(mktemp)
#capture_io "$TMP_FILE" main | snack ghci
#diff golden $TMP_FILE
#rm $TMP_FILE
SNACK="snack --package-yaml ./package.yaml" test