1
1
mirror of https://github.com/nmattia/snack.git synced 2024-11-24 12:16:17 +03:00

Major build interface refactor

This commit is contained in:
Nicolas Mattia 2019-01-29 22:15:09 +01:00
parent 88f6a41029
commit ecefcc15dd
9 changed files with 124 additions and 183 deletions

View File

@ -18,6 +18,7 @@ import Control.Monad.IO.Class
import Data.Aeson (FromJSON, (.:))
import Data.FileEmbed (embedStringFile)
import Data.List (intercalate)
import Data.Maybe (mapMaybe)
import Data.Semigroup ((<>))
import Data.String.Interpolate
import Shelly (Sh)
@ -283,7 +284,6 @@ newtype ModuleName = ModuleName T.Text
data BuildResult
= BuiltLibrary LibraryBuild
| BuiltExecutable ExecutableBuild
| BuiltMulti MultiBuild
| BuiltGhci GhciBuild
deriving Show
@ -291,7 +291,6 @@ 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
@ -304,10 +303,7 @@ newtype GhciBuild = GhciBuild
{ ghciExePath :: NixPath
}
deriving stock Show
instance FromJSON GhciBuild where
parseJSON = Aeson.withObject "ghci build" $ \o ->
GhciBuild <$> o .: "ghci_path"
deriving newtype FromJSON
-- The kinds of builds: library, executable, or a mix of both (currently only
-- for HPack)
@ -322,17 +318,6 @@ instance FromJSON ExecutableBuild where
parseJSON = Aeson.withObject "executable build" $ \o ->
ExecutableBuild <$> o .: "exe_path"
newtype MultiBuild = MultiBuild
{ executableBuilds :: Map.Map T.Text ExecutableBuild
}
deriving stock Show
instance Aeson.FromJSON MultiBuild where
parseJSON = Aeson.withObject "multi build" $ \o ->
MultiBuild <$> o .: "executables"
--- Type-helpers for passing arguments to Nix
data NixArg = NixArg
{ argType :: NixArgType
, argName :: T.Text
@ -418,7 +403,7 @@ nixBuild snackCfg extraNixArgs nixExpr =
: [ argName narg , argValue narg ]
nixCfg = snackNixCfg snackCfg
snackBuild :: SnackConfig -> PackageFile -> Sh BuildResult
snackBuild :: SnackConfig -> PackageFile -> Sh [BuildResult]
snackBuild snackCfg packageFile = do
NixPath out <- nixBuild snackCfg
[ NixArg
@ -441,8 +426,9 @@ snackGhci snackCfg packageFile = do
]
$ NixExpr "snack.inferGhci packageFile"
liftIO (BS.readFile (T.unpack out)) >>= decodeOrFail >>= \case
BuiltGhci g -> pure g
b -> throwIO $ userError $ "Expected GHCi build, got " <> show b
-- TODO: shouldn't be dropping the tail
(BuiltGhci g):_ -> pure g
bs -> throwIO $ userError $ "Expected GHCi build, got " <> show bs
runCommand :: SnackConfig -> PackageFile -> Command -> IO ()
runCommand snackCfg packageFile = \case
@ -455,13 +441,13 @@ runCommand snackCfg packageFile = \case
noTest :: IO a
noTest = fail "There is no test command for test suites"
runBuildResult :: [String] -> BuildResult -> IO ()
runBuildResult args = \case
BuiltExecutable (ExecutableBuild p) -> runExe p args
BuiltMulti b
| [ExecutableBuild exe] <- Map.elems (executableBuilds b) ->
runExe exe args
b -> fail $ "Unexpected build type: " <> show b
runBuildResult :: [String] -> [BuildResult] -> IO ()
runBuildResult args res =
case mapMaybe (\case {BuiltExecutable e -> Just e; _ -> Nothing}) res of
[ExecutableBuild p] -> runExe p args
-- TODO: be more graceful here
-- TODO: allow 'snack run my-exe -- ...'
_ -> fail $ "Expected exactly one executable, got: " <> show res
runExe :: NixPath -> [String] -> IO ()
runExe (NixPath fp) args = executeFile (T.unpack fp) True args Nothing

View File

@ -1,5 +1,4 @@
_: pkgs: rec {
snack-lib = pkgs.callPackage ../snack-lib/default.nix { };
snack-exe =
(snack-lib.buildAsExecutable (snack-lib.snackSpec ../bin/package.nix)).out;
snack-exe = snack-lib.executable ../bin/package.nix;
}

View File

@ -19,24 +19,106 @@ with (callPackage ./package-spec.nix {});
with (callPackage ./hpack.nix {});
let
# Derivation that creates a binary in a 'bin' folder.
executable = packageFile:
let
specs = specsFromPackageFile packageFile;
spec =
if pkgs.lib.length specs == 1
then pkgs.lib.head specs
else abort "'executable' can only be called on a single executable";
exe =
if spec.packageIsExe
then buildAsExecutable spec
else abort "'executable' called on a library";
in exe.out;
# Build a package spec as resp. a library and an executable
buildAsLibrary = pkgSpec:
buildLibrary ghcWith (libraryModSpecs pkgSpec);
buildAsExecutable = pkgSpec:
let drv = linkMainModule ghcWith (executableMainModSpec pkgSpec);
in
{ out = drv.out;
exe_path = "${drv.out}/${drv.relExePath}";
};
ghcWith = deps: ghcWithPackages
(ps: map (p: ps.${p}) deps);
# Assumes the package description describes an executable
withMainModSpec = pkgDescr: act:
specsFromPackageFile = packageFile:
let
mainModName = pkgDescr.packageMain;
mainModSpec =
basename = builtins.baseNameOf packageFile;
components = pkgs.lib.strings.splitString "." basename;
ext =
if pkgs.lib.length components <= 1
then abort ("File " ++ packageFile ++ " does not have an extension")
else pkgs.lib.last components;
fromNix = [(mkPackageSpec (import packageFile))];
fromHPack =
let
fld = moduleSpecFold' modSpecs;
modSpecs = foldDAG fld [mainModName];
in modSpecs.${mainModName};
drv = linkMainModule ghcWith mainModSpec;
in
{ out = drv.out;
outPath = "${drv.out}";
exePath = "${drv.out}/${drv.relExePath}";
};
descrs = pkgDescrsFromHPack packageFile;
executables =
builtins.map mkPackageSpec descrs.executables;
library = withAttr descrs "library" null
(comp: if builtins.isNull comp then null else mkPackageSpec comp);
in executables ++ (if builtins.isNull library then [] else [ library ]);
specs =
if ext == "nix" then fromNix
else if ext == "yaml" then fromHPack
else if ext == "yml" then fromHPack
else abort ("Unknown extension " ++ ext ++ " of file " ++ packageFile);
in specs;
# Normal build (libs, exes)
inferBuild = packageFile:
mkPackages (specsFromPackageFile packageFile);
mkPackages = pkgSpecs: writeText "build.json"
( builtins.toJSON
( builtins.map
(pkgSpec:
if pkgSpec.packageIsExe
then
{ build_type = "executable";
result = buildAsExecutable pkgSpec;
}
else
{ build_type = "library";
result = buildAsLibrary pkgSpec;
}
) pkgSpecs
)
);
# GHCi build (libs, exes)
inferGhci = packageFile:
mkPackagesGhci (specsFromPackageFile packageFile);
mkPackagesGhci = pkgSpecs: writeText "hpack-ghci-json"
( builtins.toJSON (
builtins.map
(pkgSpec:
let
drv =
if pkgSpec.packageIsExe
then ghciWithMain ghcWith (executableMainModSpec pkgSpec)
else ghciWithModules ghcWith (libraryModSpecs pkgSpec)
;
in
{ build_type = "ghci"; # TODO: need to record the name somewhere
result = "${drv.out}/bin/ghci-with-files";
}
) pkgSpecs
));
# How to build resp. libraries and executables
libraryModSpecs = pkgSpec:
let
@ -58,143 +140,13 @@ let
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}";
};
# TODO: deduplicate extensions + update README with --package-file
inferBuild = packageFile:
let
basename = builtins.baseNameOf packageFile;
components = pkgs.lib.strings.splitString "." basename;
ext =
if pkgs.lib.length components <= 1
then abort ("File " ++ packageFile ++ " does not have an extension")
else pkgs.lib.last components;
build =
if ext == "nix" then inferSnackBuild
else if ext == "yaml" then inferHPackBuild
else if ext == "yml" then inferHPackBuild
else abort ("Unknown extension " ++ ext ++ " of file " ++ packageFile);
in build packageFile;
inferGhci = packageFile:
let
basename = builtins.baseNameOf packageFile;
components = pkgs.lib.strings.splitString "." basename;
ext =
if pkgs.lib.length components <= 1
then abort ("File " ++ packageFile ++ " does not have an extension")
else pkgs.lib.last components;
ghci =
if ext == "nix" then inferSnackGhci
else if ext == "yaml" then inferHPackGhci
else if ext == "yml" then inferHPackGhci
else abort ("Unknown extension " ++ ext ++ " of file " ++ packageFile);
in ghci packageFile;
inferSnackBuild = packageNix: mkPackage (import packageNix);
inferSnackGhci = packageNix: writeText "snack-ghci-json"
( builtins.toJSON (
let
pkgSpec = mkPackageSpec (import packageNix);
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 packageNix);
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 = packageNix: mkPackageSpec (import packageNix);
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;
};
mkPackage = snackNixExpr: writeText "snack-build-json"
( builtins.toJSON (
let
pkgSpec = mkPackageSpec snackNixExpr;
in
if builtins.isNull pkgSpec.packageMain
then
{ "build_type" = "library";
"result" = buildAsLibrary pkgSpec;
}
else
{ "build_type" = "executable";
"result" = buildAsExecutable pkgSpec;
}
));
in
{
inherit
inferBuild
inferGhci
inferSnackBuild
inferSnackGhci
inferHPackBuild
inferHPackGhci
packageYaml
buildAsExecutable
buildAsLibrary
snackSpec
hpackSpecs
mkPackage
executable
;
}

View File

@ -52,9 +52,9 @@ in
);
exes =
withAttr package "executables" {} (lib.mapAttrs (k: v: mkExe v)) //
withAttr package "executable" {} (comp: { ${package.name} = mkExe comp; });
mkExe = component:
withAttr package "executables" [] (lib.mapAttrsToList (k: v: mkExe k v)) ++
withAttr package "executable" [] (comp: [(mkExe package.name comp)] );
mkExe = nn: component:
let
depOrPack =
lib.lists.partition
@ -62,8 +62,9 @@ in
(optAttr component "dependencies" []);
in
{ main = fileToModule component.main;
name = nn;
src =
let
let
base = builtins.dirOf packageYaml;
source-dirs = optAttr component "source-dirs" ".";
in

View File

@ -8,7 +8,8 @@ rec {
mkPackageSpec =
packageDescr@
{ src
{ src ? []
, name ? null
, main ? null
, ghcOpts ? []
, dependencies ? []
@ -17,7 +18,9 @@ rec {
, extra-directories ? []
, packages ? []
}:
{ packageMain = main;
{ packageIsExe = ! builtins.isNull main;
packageName = name;
packageMain = main;
packageSourceDirs =
if builtins.isList src
then src

View File

@ -6,7 +6,7 @@ set -euo pipefail
test() {
TMP_FILE=$(mktemp)
cat $($SNACK build) | jq -M '.result | keys' > $TMP_FILE
cat $($SNACK build) | jq -M '.[0] | .result | keys' > $TMP_FILE
diff golden.jq $TMP_FILE

View File

@ -6,7 +6,7 @@ set -euo pipefail
test() {
TMP_FILE=$(mktemp)
cat $($SNACK build) | jq -M '.result | keys' > $TMP_FILE
cat $($SNACK build) | jq -M '.[0] | .result | keys' > $TMP_FILE
diff golden.jq $TMP_FILE

View File

@ -6,7 +6,7 @@ set -euo pipefail
test() {
TMP_FILE=$(mktemp)
cat $($SNACK build) | jq -M '.result | keys' > $TMP_FILE
cat $($SNACK build) | jq -M '.[0] | .result | keys' > $TMP_FILE
diff golden.jq $TMP_FILE
}

View File

@ -6,7 +6,7 @@ set -euo pipefail
test() {
TMP_FILE=$(mktemp)
cat $($SNACK build) | jq -M '.result | keys' > $TMP_FILE
cat $($SNACK build) | jq -M '.[0] | .result | keys' > $TMP_FILE
diff golden.jq $TMP_FILE